blaxel 0.1.12rc47__py3-none-any.whl → 0.1.14.dev1__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.
@@ -7,6 +7,7 @@ client credentials and refresh tokens.
7
7
  from dataclasses import dataclass
8
8
  from datetime import datetime, timedelta
9
9
  from typing import Generator, Optional
10
+ import time
10
11
 
11
12
  import requests
12
13
  from httpx import Request, Response
@@ -46,14 +47,17 @@ class ClientCredentials(BlaxelAuth):
46
47
  "X-Blaxel-Workspace": self.workspace_name,
47
48
  }
48
49
 
49
- def get_token(self) -> Optional[Exception]:
50
+ def _request_token(self, remaining_retries: int = 3) -> Optional[Exception]:
50
51
  """
51
- Checks if the access token needs to be refreshed and performs the refresh if necessary.
52
+ Makes the token request with recursive retry logic.
53
+
54
+ Args:
55
+ remaining_retries (int): Number of retry attempts remaining.
52
56
 
53
57
  Returns:
54
- Optional[Exception]: An exception if refreshing fails, otherwise None.
58
+ Optional[Exception]: An exception if refreshing fails after all retries, otherwise None.
55
59
  """
56
- if self.need_token():
60
+ try:
57
61
  headers = {"Authorization": f"Basic {self.credentials.client_credentials}", "Content-Type": "application/json"}
58
62
  body = {"grant_type": "client_credentials"}
59
63
  response = requests.post(f"{self.base_url}/oauth/token", headers=headers, json=body)
@@ -63,6 +67,23 @@ class ClientCredentials(BlaxelAuth):
63
67
  self.credentials.refresh_token = creds["refresh_token"]
64
68
  self.credentials.expires_in = creds["expires_in"]
65
69
  self.expires_at = datetime.now() + timedelta(seconds=self.credentials.expires_in)
70
+ return None
71
+ except Exception as e:
72
+ if remaining_retries > 0:
73
+ time.sleep(1)
74
+ return self._request_token(remaining_retries - 1)
75
+ return e
76
+
77
+ def get_token(self) -> Optional[Exception]:
78
+ """
79
+ Checks if the access token needs to be refreshed and performs the refresh if necessary.
80
+ Uses recursive retry logic for up to 3 attempts.
81
+
82
+ Returns:
83
+ Optional[Exception]: An exception if refreshing fails after all retries, otherwise None.
84
+ """
85
+ if self.need_token():
86
+ return self._request_token()
66
87
  return None
67
88
 
68
89
  def need_token(self):
blaxel/common/internal.py CHANGED
@@ -7,21 +7,25 @@ from typing import Optional
7
7
 
8
8
  logger = getLogger(__name__)
9
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
10
+ def get_alphanumeric_limited_hash(input_str, max_size=48):
11
+ """
12
+ Create an alphanumeric hash using MD5 that can be reproduced in Go, TypeScript, and Python.
13
+
14
+ Args:
15
+ input_str (str): The input string to hash
16
+ max_size (int): The maximum length of the returned hash
17
+
18
+ Returns:
19
+ str: An alphanumeric hash of the input string, limited to max_size
20
+ """
21
+ # Calculate MD5 hash and convert to hexadecimal
22
+ hash_hex = hashlib.md5(input_str.encode('utf-8')).hexdigest()
23
+
24
+ # Limit to max_size
25
+ if len(hash_hex) > max_size:
26
+ return hash_hex[:max_size]
27
+
28
+ return hash_hex
25
29
 
26
30
 
27
31
  def get_global_unique_hash(workspace: str, type: str, name: str) -> str:
blaxel/common/settings.py CHANGED
@@ -62,7 +62,14 @@ class Settings:
62
62
  @property
63
63
  def run_internal_hostname(self) -> str:
64
64
  """Get the run internal hostname."""
65
- return os.environ.get("BL_RUN_INTERNAL_HOSTNAME", "")
65
+ if self.generation == "":
66
+ return ""
67
+ return os.environ.get("BL_RUN_INTERNAL_HOST", "")
68
+
69
+ @property
70
+ def generation(self) -> str:
71
+ """Get the generation."""
72
+ return os.environ.get("BL_GENERATION", "")
66
73
 
67
74
  @property
68
75
  def bl_cloud(self) -> bool:
@@ -0,0 +1,187 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import asyncio
5
+ import json
6
+ import requests
7
+
8
+
9
+ from typing import Any, Dict, Callable, Awaitable
10
+ from logging import getLogger
11
+ from ..client import client
12
+ from ..common.env import env
13
+ from ..common.internal import get_global_unique_hash
14
+ from ..common.settings import settings
15
+ from ..instrumentation.span import SpanManager
16
+
17
+
18
+
19
+ class BlJobWrapper:
20
+ def get_arguments(self) -> Dict[str, Any]:
21
+ if not os.getenv('BL_EXECUTION_DATA_URL'):
22
+ parser = argparse.ArgumentParser()
23
+ # Parse known args, ignore unknown
24
+ args, unknown = parser.parse_known_args()
25
+ # Convert to dict and include unknown args
26
+ args_dict = vars(args)
27
+ # Add unknown args to dict
28
+ for i in range(0, len(unknown), 2):
29
+ if i + 1 < len(unknown):
30
+ key = unknown[i].lstrip('-')
31
+ args_dict[key] = unknown[i + 1]
32
+ return args_dict
33
+
34
+ response = requests.get(os.getenv('BL_EXECUTION_DATA_URL'))
35
+ data = response.json()
36
+ tasks = data.get('tasks', [])
37
+ return tasks[self.index] if self.index < len(tasks) else {}
38
+
39
+ @property
40
+ def index_key(self) -> str:
41
+ return os.getenv('BL_TASK_KEY', 'TASK_INDEX')
42
+
43
+ @property
44
+ def index(self) -> int:
45
+ index_value = os.getenv(self.index_key)
46
+ return int(index_value) if index_value else 0
47
+
48
+ def start(self, func: Callable):
49
+ """
50
+ Run a job defined in a function, it's run in the current process.
51
+ Handles both async and sync functions.
52
+ Arguments are passed as keyword arguments to the function.
53
+ """
54
+ try:
55
+ parsed_args = self.get_arguments()
56
+ if asyncio.iscoroutinefunction(func):
57
+ asyncio.run(func(**parsed_args))
58
+ else:
59
+ func(**parsed_args)
60
+ sys.exit(0)
61
+ except Exception as error:
62
+ print('Job execution failed:', error, file=sys.stderr)
63
+ sys.exit(1)
64
+
65
+
66
+
67
+
68
+ logger = getLogger(__name__)
69
+
70
+ class BlJob:
71
+ def __init__(self, name: str):
72
+ self.name = name
73
+
74
+ @property
75
+ def internal_url(self):
76
+ """Get the internal URL for the job using a hash of workspace and job name."""
77
+ hash = get_global_unique_hash(settings.workspace, "job", self.name)
78
+ return f"{settings.run_internal_protocol}://bl-{settings.env}-{hash}.{settings.run_internal_hostname}"
79
+
80
+ @property
81
+ def forced_url(self):
82
+ """Get the forced URL from environment variables if set."""
83
+ env_var = self.name.replace("-", "_").upper()
84
+ if env[f"BL_JOB_{env_var}_URL"]:
85
+ return env[f"BL_JOB_{env_var}_URL"]
86
+ return None
87
+
88
+ @property
89
+ def external_url(self):
90
+ return f"{settings.run_url}/{settings.workspace}/jobs/{self.name}"
91
+
92
+ @property
93
+ def fallback_url(self):
94
+ if self.external_url != self.url:
95
+ return self.external_url
96
+ return None
97
+
98
+ @property
99
+ def url(self):
100
+ if self.forced_url:
101
+ return self.forced_url
102
+ if settings.run_internal_hostname:
103
+ return self.internal_url
104
+ return self.external_url
105
+
106
+ def call(self, url, input_data, headers: dict = {}, params: dict = {}):
107
+ body = {
108
+ "tasks": input_data
109
+ }
110
+
111
+ return client.get_httpx_client().post(
112
+ url+"/executions",
113
+ headers={
114
+ 'Content-Type': 'application/json',
115
+ **headers
116
+ },
117
+ json=body,
118
+ params=params
119
+ )
120
+
121
+ async def acall(self, url, input_data, headers: dict = {}, params: dict = {}):
122
+ logger.debug(f"Job Calling: {self.name}")
123
+ body = {
124
+ "tasks": input_data
125
+ }
126
+
127
+ return await client.get_async_httpx_client().post(
128
+ url+"/executions",
129
+ headers={
130
+ 'Content-Type': 'application/json',
131
+ **headers
132
+ },
133
+ json=body,
134
+ params=params
135
+ )
136
+
137
+ def run(self, input: Any, headers: dict = {}, params: dict = {}) -> str:
138
+ attributes = {
139
+ "job.name": self.name,
140
+ "span.type": "job.run",
141
+ }
142
+ with SpanManager("blaxel-tracer").create_active_span(self.name, attributes) as span:
143
+ logger.debug(f"Job Calling: {self.name}")
144
+ response = self.call(self.url, input, headers, params)
145
+ if response.status_code >= 400:
146
+ if not self.fallback_url:
147
+ span.set_attribute("job.run.error", response.text)
148
+ raise Exception(f"Job {self.name} returned status code {response.status_code} with body {response.text}")
149
+ response = self.call(self.fallback_url, input, headers, params)
150
+ if response.status_code >= 400:
151
+ span.set_attribute("job.run.error", response.text)
152
+ raise Exception(f"Job {self.name} returned status code {response.status_code} with body {response.text}")
153
+ span.set_attribute("job.run.result", response.text)
154
+ return response.text
155
+
156
+ async def arun(self, input: Any, headers: dict = {}, params: dict = {}) -> Awaitable[str]:
157
+ attributes = {
158
+ "job.name": self.name,
159
+ "span.type": "job.run",
160
+ }
161
+ with SpanManager("blaxel-tracer").create_active_span(self.name, attributes) as span:
162
+ logger.debug(f"Job Calling: {self.name}")
163
+ response = await self.acall(self.url, input, headers, params)
164
+ if response.status_code >= 400:
165
+ if not self.fallback_url:
166
+ span.set_attribute("job.run.error", response.text)
167
+ raise Exception(f"Job {self.name} returned status code {response.status_code} with body {response.text}")
168
+ response = await self.acall(self.fallback_url, input, headers, params)
169
+ if response.status_code >= 400:
170
+ span.set_attribute("job.run.error", response.text)
171
+ raise Exception(f"Job {self.name} returned status code {response.status_code} with body {response.text}")
172
+ span.set_attribute("job.run.result", response.text)
173
+ return response.text
174
+
175
+ def __str__(self):
176
+ return f"Job {self.name}"
177
+
178
+ def __repr__(self):
179
+ return self.__str__()
180
+
181
+
182
+
183
+ def bl_job(name: str):
184
+ return BlJob(name)
185
+
186
+ # Create a singleton instance
187
+ bl_start_job = BlJobWrapper()
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blaxel
3
- Version: 0.1.12rc47
3
+ Version: 0.1.14.dev1
4
4
  Summary: Add your description here
5
5
  Author-email: cploujoux <cploujoux@blaxel.ai>
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.10
8
8
  Requires-Dist: attrs>=21.3.0
9
9
  Requires-Dist: httpx>=0.27.0
10
- Requires-Dist: mcp>=1.2.1
10
+ Requires-Dist: mcp<=1.7.1
11
11
  Requires-Dist: opentelemetry-api>=1.28.0
12
12
  Requires-Dist: opentelemetry-exporter-otlp>=1.28.0
13
13
  Requires-Dist: opentelemetry-instrumentation-anthropic==0.35.0
@@ -2,7 +2,7 @@ blaxel/__init__.py,sha256=qmuJKjl5oGnjj4TbqHcJqUkKoxk4PvCsMb6-8rp67pE,159
2
2
  blaxel/agents/__init__.py,sha256=RDWkvfICIXXaQxJuuSu63jsFj_F8NBAL4U752hfN4AE,5262
3
3
  blaxel/authentication/__init__.py,sha256=tL9XKNCek5ixszTqjlKRBvidXMg4Nj6ODlBKlxxA9uk,3283
4
4
  blaxel/authentication/apikey.py,sha256=nOgLVba7EfVk3V-qm7cj-30LAL-BT7NOMIlGL9Ni1jY,1249
5
- blaxel/authentication/clientcredentials.py,sha256=SfWNuZVHZw6jjMqoBMMB4ZawmpyKbVPbOpv-JDFqzw8,3080
5
+ blaxel/authentication/clientcredentials.py,sha256=7ZrqsXTQTBmCzzrWWUl9qRqq7ZPjucBWQ6dzFCB7JT4,3804
6
6
  blaxel/authentication/devicemode.py,sha256=kWbArs4okIIDqW-ql5oV2eQRE_LpRwfadCB6LG83irw,5986
7
7
  blaxel/authentication/oauth.py,sha256=Q5J0taIK1JrvGB6BC-zz3hM77HPCNu01DPGf4l7xjPQ,1417
8
8
  blaxel/authentication/types.py,sha256=E3lmfbmZuJ4Bc_kGA0Kc0GZC02Sjha1_2CbabP7z6oo,1603
@@ -262,14 +262,15 @@ blaxel/client/models/workspace_runtime.py,sha256=dxEpmwCFPOCRKHRKhY-iW7j6TbtL5qU
262
262
  blaxel/client/models/workspace_user.py,sha256=70CcifQWYbeWG7TDui4pblTzUe5sVK0AS19vNCzKE8g,3423
263
263
  blaxel/common/autoload.py,sha256=NFuK71-IHOY2JQyEBSjDCVfUaQ8D8PJsEUEryIdG4AU,263
264
264
  blaxel/common/env.py,sha256=wTbzPDdNgz4HMJiS2NCZmQlN0qpxy1PQEYBaZgtvhoc,1247
265
- blaxel/common/internal.py,sha256=J-etgnBzelb-ln8uBR9WIWzrEUyDds8rdT9FImjes9g,2390
265
+ blaxel/common/internal.py,sha256=PExgeKfJEmjINKreNb3r2nB5GAfG7uJhbfqHxuxBED8,2395
266
266
  blaxel/common/logger.py,sha256=emqgonfZMBIaQPowpngWOOZxWQieKP-yj_OzGZT8Oe8,1918
267
- blaxel/common/settings.py,sha256=X0g0_hIR19qfjSeWNf8qraxb_UTSbYukVHJeRHKLTn8,2138
267
+ blaxel/common/settings.py,sha256=7KTryuBdud0IfHqykX7xEEtpgq5M5h1Z8YEzYKsHB-Q,2327
268
268
  blaxel/instrumentation/exporters.py,sha256=EoX3uaBVku1Rg49pSNXKFyHhgY5OV3Ih6UlqgjF5epw,1670
269
269
  blaxel/instrumentation/log.py,sha256=4tGyvLg6r4DbjqJfajYbbZ1toUzF4Q4H7kHVqYWFAEA,2537
270
270
  blaxel/instrumentation/manager.py,sha256=jVwVasyMI6tu0zv27DVYOaN57nXYuHFJ8QzJQKaNoK4,8880
271
271
  blaxel/instrumentation/map.py,sha256=zZoiUiQHmik5WQZ4VCWNARSa6ppMi0r7D6hlb41N-Mg,1589
272
272
  blaxel/instrumentation/span.py,sha256=X2lwfu_dyxwQTMQJT2vbXOrbVSChEhjRLc413QOxQJM,3244
273
+ blaxel/jobs/__init__.py,sha256=4Mk-S_eT-bV-bI9Y_TOfisv_rxme9O1Z69AgmDoAKTY,6510
273
274
  blaxel/mcp/__init__.py,sha256=KednMrtuc4Y0O3lv7u1Lla54FCk8UX9c1k0USjL3Ahk,69
274
275
  blaxel/mcp/client.py,sha256=cFFXfpKXoMu8qTUly2ejF0pX2iBQkSNAxqwvDV1V6xY,4979
275
276
  blaxel/mcp/server.py,sha256=GIldtA_NgIc2dzd7ZpPvpbhpIt_7AfKu5yS_YJ0bDGg,7310
@@ -340,7 +341,7 @@ blaxel/tools/llamaindex.py,sha256=-gQ-C9V_h9a11J4ItsbWjXrCJOg0lRKsb98v9rVsNak,71
340
341
  blaxel/tools/openai.py,sha256=GuFXkj6bXEwldyVr89jEsRAi5ihZUVEVe327QuWiGNs,653
341
342
  blaxel/tools/pydantic.py,sha256=CvnNbAG_J4yBtA-XFI4lQrq3FYKjNd39hu841vZT004,1801
342
343
  blaxel/tools/types.py,sha256=YPCGJ4vZDhqR0X2H_TWtc5chQScsC32nGTQdRKJlO8Y,707
343
- blaxel-0.1.12rc47.dist-info/METADATA,sha256=O8zx3Mg-nM1DAgF0WeleMuJigpw1ZqyuZZGp7pAcR6U,11776
344
- blaxel-0.1.12rc47.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
345
- blaxel-0.1.12rc47.dist-info/licenses/LICENSE,sha256=p5PNQvpvyDT_0aYBDgmV1fFI_vAD2aSV0wWG7VTgRis,1069
346
- blaxel-0.1.12rc47.dist-info/RECORD,,
344
+ blaxel-0.1.14.dev1.dist-info/METADATA,sha256=2gRGZUIin1mayKrvwVjU91mugnQgo9gLVWu2VkPY5rM,11777
345
+ blaxel-0.1.14.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
346
+ blaxel-0.1.14.dev1.dist-info/licenses/LICENSE,sha256=p5PNQvpvyDT_0aYBDgmV1fFI_vAD2aSV0wWG7VTgRis,1069
347
+ blaxel-0.1.14.dev1.dist-info/RECORD,,