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.
Files changed (261) hide show
  1. blaxel/__init__.py +8 -0
  2. blaxel/agents/__init__.py +5 -0
  3. blaxel/agents/chain.py +153 -0
  4. blaxel/agents/chat.py +286 -0
  5. blaxel/agents/decorator.py +208 -0
  6. blaxel/agents/thread.py +24 -0
  7. blaxel/agents/voice/openai.py +255 -0
  8. blaxel/agents/voice/utils.py +25 -0
  9. blaxel/api/__init__.py +1 -0
  10. blaxel/api/agents/__init__.py +0 -0
  11. blaxel/api/agents/create_agent.py +155 -0
  12. blaxel/api/agents/delete_agent.py +146 -0
  13. blaxel/api/agents/get_agent.py +146 -0
  14. blaxel/api/agents/get_agent_logs.py +151 -0
  15. blaxel/api/agents/get_agent_metrics.py +150 -0
  16. blaxel/api/agents/get_agent_trace_ids.py +201 -0
  17. blaxel/api/agents/list_agent_revisions.py +155 -0
  18. blaxel/api/agents/list_agents.py +127 -0
  19. blaxel/api/agents/update_agent.py +168 -0
  20. blaxel/api/configurations/__init__.py +0 -0
  21. blaxel/api/configurations/get_configuration.py +122 -0
  22. blaxel/api/default/__init__.py +0 -0
  23. blaxel/api/default/get_trace.py +150 -0
  24. blaxel/api/default/get_trace_ids.py +218 -0
  25. blaxel/api/default/get_trace_logs.py +186 -0
  26. blaxel/api/default/list_mcp_hub_definitions.py +127 -0
  27. blaxel/api/functions/__init__.py +0 -0
  28. blaxel/api/functions/create_function.py +155 -0
  29. blaxel/api/functions/delete_function.py +146 -0
  30. blaxel/api/functions/get_function.py +146 -0
  31. blaxel/api/functions/get_function_logs.py +151 -0
  32. blaxel/api/functions/get_function_metrics.py +150 -0
  33. blaxel/api/functions/get_function_trace_ids.py +201 -0
  34. blaxel/api/functions/list_function_revisions.py +158 -0
  35. blaxel/api/functions/list_functions.py +131 -0
  36. blaxel/api/functions/update_function.py +168 -0
  37. blaxel/api/integrations/__init__.py +0 -0
  38. blaxel/api/integrations/create_integration_connection.py +167 -0
  39. blaxel/api/integrations/delete_integration_connection.py +158 -0
  40. blaxel/api/integrations/get_integration.py +97 -0
  41. blaxel/api/integrations/get_integration_connection.py +158 -0
  42. blaxel/api/integrations/get_integration_connection_model.py +104 -0
  43. blaxel/api/integrations/get_integration_connection_model_endpoint_configurations.py +97 -0
  44. blaxel/api/integrations/list_integration_connection_models.py +97 -0
  45. blaxel/api/integrations/list_integration_connections.py +139 -0
  46. blaxel/api/integrations/update_integration_connection.py +180 -0
  47. blaxel/api/invitations/__init__.py +0 -0
  48. blaxel/api/invitations/list_all_pending_invitations.py +142 -0
  49. blaxel/api/knowledgebases/__init__.py +0 -0
  50. blaxel/api/knowledgebases/create_knowledgebase.py +163 -0
  51. blaxel/api/knowledgebases/delete_knowledgebase.py +154 -0
  52. blaxel/api/knowledgebases/get_knowledgebase.py +154 -0
  53. blaxel/api/knowledgebases/list_knowledgebase_revisions.py +158 -0
  54. blaxel/api/knowledgebases/list_knowledgebases.py +139 -0
  55. blaxel/api/knowledgebases/update_knowledgebase.py +176 -0
  56. blaxel/api/locations/__init__.py +0 -0
  57. blaxel/api/locations/list_locations.py +139 -0
  58. blaxel/api/metrics/__init__.py +0 -0
  59. blaxel/api/metrics/get_metrics.py +130 -0
  60. blaxel/api/models/__init__.py +0 -0
  61. blaxel/api/models/create_model.py +163 -0
  62. blaxel/api/models/delete_model.py +154 -0
  63. blaxel/api/models/get_model.py +154 -0
  64. blaxel/api/models/get_model_logs.py +155 -0
  65. blaxel/api/models/get_model_metrics.py +158 -0
  66. blaxel/api/models/get_model_trace_ids.py +201 -0
  67. blaxel/api/models/list_model_revisions.py +158 -0
  68. blaxel/api/models/list_models.py +135 -0
  69. blaxel/api/models/update_model.py +176 -0
  70. blaxel/api/policies/__init__.py +0 -0
  71. blaxel/api/policies/create_policy.py +167 -0
  72. blaxel/api/policies/delete_policy.py +154 -0
  73. blaxel/api/policies/get_policy.py +154 -0
  74. blaxel/api/policies/list_policies.py +139 -0
  75. blaxel/api/policies/update_policy.py +180 -0
  76. blaxel/api/privateclusters/__init__.py +0 -0
  77. blaxel/api/privateclusters/create_private_cluster.py +132 -0
  78. blaxel/api/privateclusters/delete_private_cluster.py +156 -0
  79. blaxel/api/privateclusters/get_private_cluster.py +159 -0
  80. blaxel/api/privateclusters/get_private_cluster_health.py +97 -0
  81. blaxel/api/privateclusters/list_private_clusters.py +140 -0
  82. blaxel/api/privateclusters/update_private_cluster.py +156 -0
  83. blaxel/api/privateclusters/update_private_cluster_health.py +97 -0
  84. blaxel/api/service_accounts/__init__.py +0 -0
  85. blaxel/api/service_accounts/create_api_key_for_service_account.py +177 -0
  86. blaxel/api/service_accounts/create_workspace_service_account.py +170 -0
  87. blaxel/api/service_accounts/delete_api_key_for_service_account.py +104 -0
  88. blaxel/api/service_accounts/delete_workspace_service_account.py +160 -0
  89. blaxel/api/service_accounts/get_workspace_service_accounts.py +141 -0
  90. blaxel/api/service_accounts/list_api_keys_for_service_account.py +163 -0
  91. blaxel/api/service_accounts/update_workspace_service_account.py +183 -0
  92. blaxel/api/store/__init__.py +0 -0
  93. blaxel/api/store/get_store_agent.py +146 -0
  94. blaxel/api/store/get_store_function.py +146 -0
  95. blaxel/api/store/list_store_agents.py +131 -0
  96. blaxel/api/store/list_store_functions.py +131 -0
  97. blaxel/api/workspaces/__init__.py +0 -0
  98. blaxel/api/workspaces/accept_workspace_invitation.py +161 -0
  99. blaxel/api/workspaces/create_worspace.py +163 -0
  100. blaxel/api/workspaces/decline_workspace_invitation.py +158 -0
  101. blaxel/api/workspaces/delete_workspace.py +154 -0
  102. blaxel/api/workspaces/get_workspace.py +154 -0
  103. blaxel/api/workspaces/invite_workspace_user.py +174 -0
  104. blaxel/api/workspaces/leave_workspace.py +161 -0
  105. blaxel/api/workspaces/list_workspace_users.py +139 -0
  106. blaxel/api/workspaces/list_workspaces.py +139 -0
  107. blaxel/api/workspaces/remove_workspace_user.py +101 -0
  108. blaxel/api/workspaces/update_workspace.py +176 -0
  109. blaxel/api/workspaces/update_workspace_user_role.py +187 -0
  110. blaxel/authentication/__init__.py +45 -0
  111. blaxel/authentication/apikey.py +50 -0
  112. blaxel/authentication/authentication.py +176 -0
  113. blaxel/authentication/clientcredentials.py +103 -0
  114. blaxel/authentication/credentials.py +295 -0
  115. blaxel/authentication/device_mode.py +197 -0
  116. blaxel/client.py +281 -0
  117. blaxel/common/__init__.py +17 -0
  118. blaxel/common/error.py +27 -0
  119. blaxel/common/instrumentation.py +317 -0
  120. blaxel/common/logger.py +60 -0
  121. blaxel/common/secrets.py +39 -0
  122. blaxel/common/settings.py +150 -0
  123. blaxel/common/slugify.py +18 -0
  124. blaxel/common/utils.py +34 -0
  125. blaxel/deploy/__init__.py +8 -0
  126. blaxel/deploy/deploy.py +316 -0
  127. blaxel/deploy/format.py +46 -0
  128. blaxel/deploy/parser.py +192 -0
  129. blaxel/errors.py +16 -0
  130. blaxel/functions/__init__.py +7 -0
  131. blaxel/functions/common.py +228 -0
  132. blaxel/functions/decorator.py +64 -0
  133. blaxel/functions/local/local.py +48 -0
  134. blaxel/functions/mcp/client.py +96 -0
  135. blaxel/functions/mcp/mcp.py +168 -0
  136. blaxel/functions/mcp/utils.py +56 -0
  137. blaxel/functions/remote/remote.py +183 -0
  138. blaxel/models/__init__.py +233 -0
  139. blaxel/models/acl.py +133 -0
  140. blaxel/models/agent.py +126 -0
  141. blaxel/models/agent_chain.py +88 -0
  142. blaxel/models/agent_spec.py +346 -0
  143. blaxel/models/api_key.py +142 -0
  144. blaxel/models/configuration.py +85 -0
  145. blaxel/models/continent.py +70 -0
  146. blaxel/models/core_event.py +97 -0
  147. blaxel/models/core_spec.py +249 -0
  148. blaxel/models/core_spec_configurations.py +77 -0
  149. blaxel/models/country.py +70 -0
  150. blaxel/models/create_api_key_for_service_account_body.py +69 -0
  151. blaxel/models/create_workspace_service_account_body.py +71 -0
  152. blaxel/models/create_workspace_service_account_response_200.py +105 -0
  153. blaxel/models/delete_workspace_service_account_response_200.py +96 -0
  154. blaxel/models/entrypoint.py +96 -0
  155. blaxel/models/entrypoint_env.py +45 -0
  156. blaxel/models/flavor.py +70 -0
  157. blaxel/models/form.py +120 -0
  158. blaxel/models/form_config.py +45 -0
  159. blaxel/models/form_oauthomitempty.py +45 -0
  160. blaxel/models/form_secrets.py +45 -0
  161. blaxel/models/function.py +126 -0
  162. blaxel/models/function_kit.py +97 -0
  163. blaxel/models/function_spec.py +310 -0
  164. blaxel/models/get_trace_ids_response_200.py +45 -0
  165. blaxel/models/get_trace_logs_response_200.py +45 -0
  166. blaxel/models/get_trace_response_200.py +45 -0
  167. blaxel/models/get_workspace_service_accounts_response_200_item.py +96 -0
  168. blaxel/models/histogram_bucket.py +79 -0
  169. blaxel/models/histogram_stats.py +88 -0
  170. blaxel/models/integration_connection.py +96 -0
  171. blaxel/models/integration_connection_spec.py +114 -0
  172. blaxel/models/integration_connection_spec_config.py +45 -0
  173. blaxel/models/integration_connection_spec_secret.py +45 -0
  174. blaxel/models/integration_model.py +162 -0
  175. blaxel/models/integration_repository.py +88 -0
  176. blaxel/models/invite_workspace_user_body.py +60 -0
  177. blaxel/models/knowledgebase.py +126 -0
  178. blaxel/models/knowledgebase_spec.py +163 -0
  179. blaxel/models/knowledgebase_spec_options.py +45 -0
  180. blaxel/models/last_n_requests_metric.py +79 -0
  181. blaxel/models/latency_metric.py +144 -0
  182. blaxel/models/location_response.py +113 -0
  183. blaxel/models/mcp_definition.py +188 -0
  184. blaxel/models/mcp_definition_entrypoint.py +45 -0
  185. blaxel/models/mcp_definition_form.py +45 -0
  186. blaxel/models/metadata.py +139 -0
  187. blaxel/models/metadata_labels.py +45 -0
  188. blaxel/models/metric.py +79 -0
  189. blaxel/models/metrics.py +169 -0
  190. blaxel/models/metrics_models.py +45 -0
  191. blaxel/models/metrics_request_total_per_code.py +45 -0
  192. blaxel/models/metrics_rps_per_code.py +45 -0
  193. blaxel/models/model.py +126 -0
  194. blaxel/models/model_private_cluster.py +79 -0
  195. blaxel/models/model_spec.py +249 -0
  196. blaxel/models/o_auth.py +72 -0
  197. blaxel/models/owner_fields.py +70 -0
  198. blaxel/models/pending_invitation.py +124 -0
  199. blaxel/models/pending_invitation_accept.py +85 -0
  200. blaxel/models/pending_invitation_render.py +147 -0
  201. blaxel/models/pending_invitation_render_invited_by.py +88 -0
  202. blaxel/models/pending_invitation_render_workspace.py +70 -0
  203. blaxel/models/pending_invitation_workspace_details.py +72 -0
  204. blaxel/models/pod_template_spec.py +45 -0
  205. blaxel/models/policy.py +96 -0
  206. blaxel/models/policy_location.py +70 -0
  207. blaxel/models/policy_max_tokens.py +106 -0
  208. blaxel/models/policy_spec.py +151 -0
  209. blaxel/models/private_cluster.py +183 -0
  210. blaxel/models/private_location.py +61 -0
  211. blaxel/models/repository.py +70 -0
  212. blaxel/models/request_duration_over_time_metric.py +97 -0
  213. blaxel/models/request_duration_over_time_metrics.py +80 -0
  214. blaxel/models/request_total_by_origin_metric.py +115 -0
  215. blaxel/models/request_total_by_origin_metric_request_total_by_origin.py +45 -0
  216. blaxel/models/request_total_by_origin_metric_request_total_by_origin_and_code.py +45 -0
  217. blaxel/models/request_total_metric.py +123 -0
  218. blaxel/models/request_total_metric_request_total_per_code.py +45 -0
  219. blaxel/models/request_total_metric_rps_per_code.py +45 -0
  220. blaxel/models/resource_log.py +79 -0
  221. blaxel/models/resource_metrics.py +270 -0
  222. blaxel/models/resource_metrics_request_total_per_code.py +45 -0
  223. blaxel/models/resource_metrics_rps_per_code.py +45 -0
  224. blaxel/models/revision_configuration.py +97 -0
  225. blaxel/models/revision_metadata.py +124 -0
  226. blaxel/models/runtime.py +196 -0
  227. blaxel/models/runtime_startup_probe.py +45 -0
  228. blaxel/models/serverless_config.py +80 -0
  229. blaxel/models/spec_configuration.py +70 -0
  230. blaxel/models/store_agent.py +178 -0
  231. blaxel/models/store_agent_labels.py +45 -0
  232. blaxel/models/store_configuration.py +151 -0
  233. blaxel/models/store_configuration_option.py +79 -0
  234. blaxel/models/store_function.py +211 -0
  235. blaxel/models/store_function_kit.py +97 -0
  236. blaxel/models/store_function_labels.py +45 -0
  237. blaxel/models/store_function_parameter.py +88 -0
  238. blaxel/models/time_fields.py +70 -0
  239. blaxel/models/token_rate_metric.py +88 -0
  240. blaxel/models/token_rate_metrics.py +120 -0
  241. blaxel/models/token_total_metric.py +106 -0
  242. blaxel/models/trace_ids_response.py +45 -0
  243. blaxel/models/update_workspace_service_account_body.py +69 -0
  244. blaxel/models/update_workspace_service_account_response_200.py +96 -0
  245. blaxel/models/update_workspace_user_role_body.py +60 -0
  246. blaxel/models/websocket_channel.py +88 -0
  247. blaxel/models/workspace.py +148 -0
  248. blaxel/models/workspace_labels.py +45 -0
  249. blaxel/models/workspace_user.py +115 -0
  250. blaxel/py.typed +1 -0
  251. blaxel/run.py +108 -0
  252. blaxel/serve/app.py +131 -0
  253. blaxel/serve/middlewares/__init__.py +10 -0
  254. blaxel/serve/middlewares/accesslog.py +32 -0
  255. blaxel/serve/middlewares/processtime.py +28 -0
  256. blaxel/types.py +46 -0
  257. blaxel-0.64.0.dist-info/METADATA +96 -0
  258. blaxel-0.64.0.dist-info/RECORD +261 -0
  259. blaxel-0.64.0.dist-info/WHEEL +4 -0
  260. blaxel-0.64.0.dist-info/entry_points.txt +2 -0
  261. 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)}")