blaxel 0.1.9rc36__py3-none-any.whl → 0.1.10rc38__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 (52) hide show
  1. blaxel/agents/__init__.py +52 -15
  2. blaxel/authentication/__init__.py +11 -2
  3. blaxel/authentication/devicemode.py +1 -0
  4. blaxel/common/autoload.py +0 -2
  5. blaxel/common/internal.py +75 -0
  6. blaxel/common/settings.py +6 -1
  7. blaxel/mcp/server.py +2 -1
  8. blaxel/sandbox/base.py +68 -0
  9. blaxel/sandbox/client/__init__.py +8 -0
  10. blaxel/sandbox/client/api/__init__.py +1 -0
  11. blaxel/sandbox/client/api/filesystem/__init__.py +0 -0
  12. blaxel/sandbox/client/api/filesystem/delete_filesystem_path.py +184 -0
  13. blaxel/sandbox/client/api/filesystem/get_filesystem_path.py +184 -0
  14. blaxel/sandbox/client/api/filesystem/put_filesystem_path.py +189 -0
  15. blaxel/sandbox/client/api/network/__init__.py +0 -0
  16. blaxel/sandbox/client/api/network/delete_network_process_pid_monitor.py +169 -0
  17. blaxel/sandbox/client/api/network/get_network_process_pid_ports.py +169 -0
  18. blaxel/sandbox/client/api/network/post_network_process_pid_monitor.py +195 -0
  19. blaxel/sandbox/client/api/process/__init__.py +0 -0
  20. blaxel/sandbox/client/api/process/delete_process_identifier.py +163 -0
  21. blaxel/sandbox/client/api/process/delete_process_identifier_kill.py +189 -0
  22. blaxel/sandbox/client/api/process/get_process.py +135 -0
  23. blaxel/sandbox/client/api/process/get_process_identifier.py +159 -0
  24. blaxel/sandbox/client/api/process/get_process_identifier_logs.py +167 -0
  25. blaxel/sandbox/client/api/process/post_process.py +176 -0
  26. blaxel/sandbox/client/client.py +162 -0
  27. blaxel/sandbox/client/errors.py +16 -0
  28. blaxel/sandbox/client/models/__init__.py +35 -0
  29. blaxel/sandbox/client/models/delete_network_process_pid_monitor_response_200.py +45 -0
  30. blaxel/sandbox/client/models/directory.py +110 -0
  31. blaxel/sandbox/client/models/error_response.py +60 -0
  32. blaxel/sandbox/client/models/file.py +105 -0
  33. blaxel/sandbox/client/models/file_request.py +78 -0
  34. blaxel/sandbox/client/models/file_with_content.py +114 -0
  35. blaxel/sandbox/client/models/get_network_process_pid_ports_response_200.py +45 -0
  36. blaxel/sandbox/client/models/get_process_identifier_logs_response_200.py +45 -0
  37. blaxel/sandbox/client/models/port_monitor_request.py +60 -0
  38. blaxel/sandbox/client/models/post_network_process_pid_monitor_response_200.py +45 -0
  39. blaxel/sandbox/client/models/process_kill_request.py +60 -0
  40. blaxel/sandbox/client/models/process_request.py +118 -0
  41. blaxel/sandbox/client/models/process_response.py +123 -0
  42. blaxel/sandbox/client/models/success_response.py +69 -0
  43. blaxel/sandbox/client/py.typed +1 -0
  44. blaxel/sandbox/client/types.py +46 -0
  45. blaxel/sandbox/filesystem.py +102 -0
  46. blaxel/sandbox/process.py +57 -0
  47. blaxel/sandbox/sandbox.py +92 -0
  48. blaxel/tools/__init__.py +62 -21
  49. {blaxel-0.1.9rc36.dist-info → blaxel-0.1.10rc38.dist-info}/METADATA +1 -1
  50. {blaxel-0.1.9rc36.dist-info → blaxel-0.1.10rc38.dist-info}/RECORD +52 -11
  51. {blaxel-0.1.9rc36.dist-info → blaxel-0.1.10rc38.dist-info}/WHEEL +0 -0
  52. {blaxel-0.1.9rc36.dist-info → blaxel-0.1.10rc38.dist-info}/licenses/LICENSE +0 -0
blaxel/agents/__init__.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  import json
3
2
  from logging import getLogger
4
3
  from typing import Any, Awaitable
@@ -8,6 +7,7 @@ from ..client import client
8
7
  from ..client.api.agents import get_agent
9
8
  from ..client.models import Agent
10
9
  from ..common.env import env
10
+ from ..common.internal import get_global_unique_hash
11
11
  from ..common.settings import settings
12
12
  from ..instrumentation.span import SpanManager
13
13
 
@@ -17,6 +17,19 @@ class BlAgent:
17
17
  def __init__(self, name: str):
18
18
  self.name = name
19
19
 
20
+ @property
21
+ def internal_url(self):
22
+ """Get the internal URL for the agent using a hash of workspace and agent name."""
23
+ hash = get_global_unique_hash(settings.workspace, "agent", self.name)
24
+ return f"{settings.run_internal_protocol}://bl-{settings.env}-{hash}.{settings.run_internal_hostname}"
25
+
26
+ @property
27
+ def forced_url(self):
28
+ """Get the forced URL from environment variables if set."""
29
+ env_var = self.name.replace("-", "_").upper()
30
+ if env[f"BL_AGENT_{env_var}_URL"]:
31
+ return env[f"BL_AGENT_{env_var}_URL"]
32
+ return None
20
33
 
21
34
  @property
22
35
  def external_url(self):
@@ -30,11 +43,10 @@ class BlAgent:
30
43
 
31
44
  @property
32
45
  def url(self):
33
- env_var = self.name.replace("-", "_").upper()
34
- if env[f"BL_AGENT_{env_var}_URL"]:
35
- return env[f"BL_AGENT_{env_var}_URL"]
36
- if f"BL_AGENT_{env_var}_SERVICE_NAME" in settings.env:
37
- return f"https://{settings.env[f'BL_AGENT_{env_var}_SERVICE_NAME']}.{settings.run_internal_hostname}"
46
+ if self.forced_url:
47
+ return self.forced_url
48
+ if settings.run_internal_hostname:
49
+ return self.internal_url
38
50
  return self.external_url
39
51
 
40
52
  def call(self, url, input_data, headers: dict = {}, params: dict = {}):
@@ -52,14 +64,14 @@ class BlAgent:
52
64
  params=params
53
65
  )
54
66
 
55
- async def acall(self, input_data, headers: dict = {}, params: dict = {}):
67
+ async def acall(self, url, input_data, headers: dict = {}, params: dict = {}):
56
68
  logger.debug(f"Agent Calling: {self.name}")
57
69
  body = input_data
58
70
  if not isinstance(body, str):
59
71
  body = json.dumps(body)
60
72
 
61
73
  return await client.get_async_httpx_client().post(
62
- self.url,
74
+ url,
63
75
  headers={
64
76
  'Content-Type': 'application/json',
65
77
  **headers
@@ -69,19 +81,44 @@ class BlAgent:
69
81
  )
70
82
 
71
83
  def run(self, input: Any, headers: dict = {}, params: dict = {}) -> str:
72
- with SpanManager("blaxel-tracer").create_active_span(self.name, {"agent.name": self.name, "agent.args": json.dumps(input)}):
84
+ attributes = {
85
+ "agent.name": self.name,
86
+ "agent.args": json.dumps(input),
87
+ "span.type": "agent.run",
88
+ }
89
+ with SpanManager("blaxel-tracer").create_active_span(self.name, attributes) as span:
73
90
  logger.debug(f"Agent Calling: {self.name}")
74
91
  response = self.call(self.url, input, headers, params)
75
92
  if response.status_code >= 400:
76
- raise Exception(f"Agent {self.name} returned status code {response.status_code} with body {response.text}")
93
+ if not self.fallback_url:
94
+ span.set_attribute("agent.run.error", response.text)
95
+ raise Exception(f"Agent {self.name} returned status code {response.status_code} with body {response.text}")
96
+ response = self.call(self.fallback_url, input, headers, params)
97
+ if response.status_code >= 400:
98
+ span.set_attribute("agent.run.error", response.text)
99
+ raise Exception(f"Agent {self.name} returned status code {response.status_code} with body {response.text}")
100
+ span.set_attribute("agent.run.result", response.text)
77
101
  return response.text
78
102
 
79
103
  async def arun(self, input: Any, headers: dict = {}, params: dict = {}) -> Awaitable[str]:
80
- logger.debug(f"Agent Calling: {self.name}")
81
- response = await self.acall(input, headers, params)
82
- if response.status_code >= 400:
83
- raise Exception(f"Agent {self.name} returned status code {response.status_code} with body {response.text}")
84
- return response.text
104
+ attributes = {
105
+ "agent.name": self.name,
106
+ "agent.args": json.dumps(input),
107
+ "span.type": "agent.run",
108
+ }
109
+ with SpanManager("blaxel-tracer").create_active_span(self.name, attributes) as span:
110
+ logger.debug(f"Agent Calling: {self.name}")
111
+ response = await self.acall(self.url, input, headers, params)
112
+ if response.status_code >= 400:
113
+ if not self.fallback_url:
114
+ span.set_attribute("agent.run.error", response.text)
115
+ raise Exception(f"Agent {self.name} returned status code {response.status_code} with body {response.text}")
116
+ response = await self.acall(self.fallback_url, input, headers, params)
117
+ if response.status_code >= 400:
118
+ span.set_attribute("agent.run.error", response.text)
119
+ raise Exception(f"Agent {self.name} returned status code {response.status_code} with body {response.text}")
120
+ span.set_attribute("agent.run.result", response.text)
121
+ return response.text
85
122
 
86
123
  def __str__(self):
87
124
  return f"Agent {self.name}"
@@ -19,16 +19,25 @@ def get_credentials() -> Optional[CredentialsType]:
19
19
  Returns:
20
20
  Optional[CredentialsType]: The credentials or None if not found
21
21
  """
22
+ def get_workspace():
23
+ if os.environ.get("BL_WORKSPACE"):
24
+ return os.environ.get("BL_WORKSPACE")
25
+ home_dir = Path.home()
26
+ config_path = home_dir / '.blaxel' / 'config.yaml'
27
+ with open(config_path, encoding='utf-8') as f:
28
+ config_json = yaml.safe_load(f)
29
+ return config_json.get("context", {}).get("workspace")
30
+
22
31
  if os.environ.get("BL_API_KEY"):
23
32
  return CredentialsType(
24
33
  api_key=os.environ.get("BL_API_KEY"),
25
- workspace=os.environ.get("BL_WORKSPACE")
34
+ workspace=get_workspace()
26
35
  )
27
36
 
28
37
  if os.environ.get("BL_CLIENT_CREDENTIALS"):
29
38
  return CredentialsType(
30
39
  client_credentials=os.environ.get("BL_CLIENT_CREDENTIALS"),
31
- workspace=os.environ.get("BL_WORKSPACE")
40
+ workspace=get_workspace()
32
41
  )
33
42
 
34
43
  try:
@@ -117,6 +117,7 @@ class DeviceMode(BlaxelAuth):
117
117
  current_time = datetime.now()
118
118
  # Refresh if token expires in less than 10 minutes
119
119
  if current_time + timedelta(minutes=10) > exp_time:
120
+ print("Refreshing token")
120
121
  return self.do_refresh()
121
122
 
122
123
  return None
blaxel/common/autoload.py CHANGED
@@ -1,5 +1,3 @@
1
-
2
-
3
1
  from ..client import client
4
2
  from ..instrumentation.manager import telemetry_manager
5
3
  from .settings import settings
@@ -0,0 +1,75 @@
1
+ import base64
2
+ import hashlib
3
+ import os
4
+ import re
5
+ from logging import getLogger
6
+ from typing import Optional
7
+
8
+ logger = getLogger(__name__)
9
+
10
+ def get_alphanumeric_limited_hash(input_str, max_size):
11
+ # Create SHA-256 hash of the input string
12
+ hash_obj = hashlib.sha256(input_str.encode('utf-8'))
13
+
14
+ # Get the hash digest in base64 format
15
+ hash_base64 = base64.b64encode(hash_obj.digest()).decode('utf-8')
16
+
17
+ # Remove non-alphanumeric characters and convert to lowercase
18
+ alphanumeric = re.sub(r'[^a-zA-Z0-9]', '', hash_base64).lower()
19
+
20
+ # Skip the first character to match the Node.js crypto output
21
+ alphanumeric = alphanumeric[1:]
22
+
23
+ # Limit to max_size characters
24
+ return alphanumeric[:max_size] if len(alphanumeric) > max_size else alphanumeric
25
+
26
+
27
+ def get_global_unique_hash(workspace: str, type: str, name: str) -> str:
28
+ """
29
+ Generate a unique hash for a combination of workspace, type, and name.
30
+
31
+ Args:
32
+ workspace: The workspace identifier
33
+ type: The type identifier
34
+ name: The name identifier
35
+
36
+ Returns:
37
+ A unique alphanumeric hash string of maximum length 48
38
+ """
39
+ global_unique_name = f"{workspace}-{type}-{name}"
40
+ hash = get_alphanumeric_limited_hash(global_unique_name, 48)
41
+ return hash
42
+
43
+ class Agent:
44
+ def __init__(self, agent_name: str, workspace: str, run_internal_protocol: str, run_internal_hostname: str):
45
+ self.agent_name = agent_name
46
+ self.workspace = workspace
47
+ self.run_internal_protocol = run_internal_protocol
48
+ self.run_internal_hostname = run_internal_hostname
49
+
50
+ @property
51
+ def internal_url(self) -> str:
52
+ """
53
+ Generate the internal URL for the agent using a unique hash.
54
+
55
+ Returns:
56
+ The internal URL as a string
57
+ """
58
+ hash_value = get_global_unique_hash(
59
+ self.workspace,
60
+ "agent",
61
+ self.agent_name
62
+ )
63
+ return f"{self.run_internal_protocol}://{hash_value}.{self.run_internal_hostname}"
64
+
65
+ @property
66
+ def forced_url(self) -> Optional[str]:
67
+ """
68
+ Check for a forced URL in environment variables.
69
+
70
+ Returns:
71
+ The forced URL if found in environment variables, None otherwise
72
+ """
73
+ env_var = self.agent_name.replace("-", "_").upper()
74
+ env_key = f"BL_AGENT_{env_var}_URL"
75
+ return os.environ.get(env_key)
blaxel/common/settings.py CHANGED
@@ -21,7 +21,7 @@ class Settings:
21
21
  @property
22
22
  def log_level(self) -> str:
23
23
  """Get the log level."""
24
- return os.environ.get("LOG_LEVEL", "INFO")
24
+ return os.environ.get("LOG_LEVEL", "INFO").upper()
25
25
 
26
26
  @property
27
27
  def base_url(self) -> str:
@@ -69,6 +69,11 @@ class Settings:
69
69
  """Is running on bl cloud."""
70
70
  return os.environ.get("BL_CLOUD", "") == "true"
71
71
 
72
+ @property
73
+ def run_internal_protocol(self) -> str:
74
+ """Get the run internal protocol."""
75
+ return os.environ.get("BL_RUN_INTERNAL_PROTOCOL", "https")
76
+
72
77
  @property
73
78
  def enable_opentelemetry(self) -> bool:
74
79
  """Get the enable opentelemetry."""
blaxel/mcp/server.py CHANGED
@@ -65,7 +65,8 @@ class BlaxelMcpServerTransport:
65
65
  "mcp.message.parsed": True,
66
66
  "mcp.method": getattr(msg, "method", None),
67
67
  "mcp.messageId": getattr(msg, "id", None),
68
- "mcp.toolName": getattr(getattr(msg, "params", None), "name", None)
68
+ "mcp.toolName": getattr(getattr(msg, "params", None), "name", None),
69
+ "span.type": "mcp.message",
69
70
  })
70
71
  self.spans[client_id+":"+msg.id] = span
71
72
  await read_stream_writer.send(msg)
blaxel/sandbox/base.py ADDED
@@ -0,0 +1,68 @@
1
+ import os
2
+
3
+ from httpx import Response
4
+
5
+ from ..client.models import Sandbox
6
+ from ..common.internal import get_global_unique_hash
7
+ from ..common.settings import settings
8
+ from .client.client import client
9
+ from .client.models import ErrorResponse
10
+
11
+
12
+ class ResponseError(Exception):
13
+ def __init__(self, response: Response):
14
+ self.status_code = response.status_code
15
+ self.status_text = response.content
16
+ self.error = None
17
+ data_error = {
18
+ "status": response.status_code,
19
+ "statusText": response.content,
20
+ }
21
+ if hasattr(response, "parsed") and isinstance(response.parsed, ErrorResponse):
22
+ data_error["error"] = response.parsed.error
23
+ self.error = response.parsed.error
24
+ super().__init__(str(data_error))
25
+
26
+
27
+ class SandboxHandleBase:
28
+ def __init__(self, sandbox: Sandbox):
29
+ self.sandbox = sandbox
30
+ self.client = client.with_base_url(self.url).with_headers(settings.headers)
31
+
32
+ @property
33
+ def name(self):
34
+ return self.sandbox.metadata and self.sandbox.metadata.name
35
+
36
+ @property
37
+ def fallback_url(self):
38
+ if self.external_url != self.url:
39
+ return self.external_url
40
+ return None
41
+
42
+ @property
43
+ def external_url(self):
44
+ return f"{settings.run_url}/{settings.workspace}/sandboxes/{self.name}"
45
+
46
+ @property
47
+ def internal_url(self):
48
+ hash_ = get_global_unique_hash(settings.workspace, "sandbox", self.name)
49
+ return f"{settings.run_internal_protocol}://bl-{settings.env}-{hash_}.{settings.run_internal_hostname}"
50
+
51
+ @property
52
+ def forced_url(self):
53
+ env_var = self.name.replace("-", "_").upper()
54
+ env_name = f"BL_SANDBOX_{env_var}_URL"
55
+ return os.environ.get(env_name)
56
+
57
+ @property
58
+ def url(self):
59
+ if self.forced_url:
60
+ return self.forced_url
61
+ if settings.run_internal_hostname:
62
+ return self.internal_url
63
+ return self.external_url
64
+
65
+ def handle_response(self, response: Response):
66
+ if response.status_code >= 400:
67
+ raise ResponseError(response)
68
+
@@ -0,0 +1,8 @@
1
+ """A client library for accessing Sandbox API"""
2
+
3
+ from .client import Client, client
4
+
5
+ __all__ = (
6
+ "Client",
7
+ "client",
8
+ )
@@ -0,0 +1 @@
1
+ """Contains methods for accessing the API"""
File without changes
@@ -0,0 +1,184 @@
1
+ from http import HTTPStatus
2
+ from typing import Any, Optional, Union
3
+
4
+ import httpx
5
+
6
+ from ... import errors
7
+ from ...client import Client
8
+ from ...models.error_response import ErrorResponse
9
+ from ...models.success_response import SuccessResponse
10
+ from ...types import UNSET, Response, Unset
11
+
12
+
13
+ def _get_kwargs(
14
+ path: str,
15
+ *,
16
+ recursive: Union[Unset, bool] = UNSET,
17
+ ) -> dict[str, Any]:
18
+ params: dict[str, Any] = {}
19
+
20
+ params["recursive"] = recursive
21
+
22
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
23
+
24
+ _kwargs: dict[str, Any] = {
25
+ "method": "delete",
26
+ "url": f"/filesystem/{path}",
27
+ "params": params,
28
+ }
29
+
30
+ return _kwargs
31
+
32
+
33
+ def _parse_response(*, client: Client, response: httpx.Response) -> Optional[Union[ErrorResponse, SuccessResponse]]:
34
+ if response.status_code == 200:
35
+ response_200 = SuccessResponse.from_dict(response.json())
36
+
37
+ return response_200
38
+ if response.status_code == 404:
39
+ response_404 = ErrorResponse.from_dict(response.json())
40
+
41
+ return response_404
42
+ if response.status_code == 500:
43
+ response_500 = ErrorResponse.from_dict(response.json())
44
+
45
+ return response_500
46
+ if client.raise_on_unexpected_status:
47
+ raise errors.UnexpectedStatus(response.status_code, response.content)
48
+ else:
49
+ return None
50
+
51
+
52
+ def _build_response(*, client: Client, response: httpx.Response) -> Response[Union[ErrorResponse, SuccessResponse]]:
53
+ return Response(
54
+ status_code=HTTPStatus(response.status_code),
55
+ content=response.content,
56
+ headers=response.headers,
57
+ parsed=_parse_response(client=client, response=response),
58
+ )
59
+
60
+
61
+ def sync_detailed(
62
+ path: str,
63
+ *,
64
+ client: Union[Client],
65
+ recursive: Union[Unset, bool] = UNSET,
66
+ ) -> Response[Union[ErrorResponse, SuccessResponse]]:
67
+ """Delete file or directory
68
+
69
+ Delete a file or directory
70
+
71
+ Args:
72
+ path (str):
73
+ recursive (Union[Unset, bool]):
74
+
75
+ Raises:
76
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
77
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
78
+
79
+ Returns:
80
+ Response[Union[ErrorResponse, SuccessResponse]]
81
+ """
82
+
83
+ kwargs = _get_kwargs(
84
+ path=path,
85
+ recursive=recursive,
86
+ )
87
+
88
+ response = client.get_httpx_client().request(
89
+ **kwargs,
90
+ )
91
+
92
+ return _build_response(client=client, response=response)
93
+
94
+
95
+ def sync(
96
+ path: str,
97
+ *,
98
+ client: Union[Client],
99
+ recursive: Union[Unset, bool] = UNSET,
100
+ ) -> Optional[Union[ErrorResponse, SuccessResponse]]:
101
+ """Delete file or directory
102
+
103
+ Delete a file or directory
104
+
105
+ Args:
106
+ path (str):
107
+ recursive (Union[Unset, bool]):
108
+
109
+ Raises:
110
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
111
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
112
+
113
+ Returns:
114
+ Union[ErrorResponse, SuccessResponse]
115
+ """
116
+
117
+ return sync_detailed(
118
+ path=path,
119
+ client=client,
120
+ recursive=recursive,
121
+ ).parsed
122
+
123
+
124
+ async def asyncio_detailed(
125
+ path: str,
126
+ *,
127
+ client: Union[Client],
128
+ recursive: Union[Unset, bool] = UNSET,
129
+ ) -> Response[Union[ErrorResponse, SuccessResponse]]:
130
+ """Delete file or directory
131
+
132
+ Delete a file or directory
133
+
134
+ Args:
135
+ path (str):
136
+ recursive (Union[Unset, bool]):
137
+
138
+ Raises:
139
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
140
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
141
+
142
+ Returns:
143
+ Response[Union[ErrorResponse, SuccessResponse]]
144
+ """
145
+
146
+ kwargs = _get_kwargs(
147
+ path=path,
148
+ recursive=recursive,
149
+ )
150
+
151
+ response = await client.get_async_httpx_client().request(**kwargs)
152
+
153
+ return _build_response(client=client, response=response)
154
+
155
+
156
+ async def asyncio(
157
+ path: str,
158
+ *,
159
+ client: Union[Client],
160
+ recursive: Union[Unset, bool] = UNSET,
161
+ ) -> Optional[Union[ErrorResponse, SuccessResponse]]:
162
+ """Delete file or directory
163
+
164
+ Delete a file or directory
165
+
166
+ Args:
167
+ path (str):
168
+ recursive (Union[Unset, bool]):
169
+
170
+ Raises:
171
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
172
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
173
+
174
+ Returns:
175
+ Union[ErrorResponse, SuccessResponse]
176
+ """
177
+
178
+ return (
179
+ await asyncio_detailed(
180
+ path=path,
181
+ client=client,
182
+ recursive=recursive,
183
+ )
184
+ ).parsed