blaxel 0.1.13__py3-none-any.whl → 0.1.14__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/logger.py CHANGED
@@ -2,8 +2,55 @@
2
2
  This module provides a custom colored formatter for logging and an initialization function
3
3
  to set up logging configurations for Blaxel applications.
4
4
  """
5
-
5
+ import json
6
6
  import logging
7
+ import os
8
+ from opentelemetry import trace
9
+
10
+ class JsonFormatter(logging.Formatter):
11
+ """
12
+ A logger compatible with standard json logging.
13
+ """
14
+ def __init__(self):
15
+ super().__init__()
16
+ self.trace_id_name = os.environ.get('BL_LOGGER_TRACE_ID', 'trace_id')
17
+ self.span_id_name = os.environ.get('BL_LOGGER_SPAN_ID', 'span_id')
18
+ self.labels_name = os.environ.get('BL_LOGGER_LABELS', 'labels')
19
+ self.trace_id_prefix = os.environ.get('BL_LOGGER_TRACE_ID_PREFIX', '')
20
+ self.span_id_prefix = os.environ.get('BL_LOGGER_SPAN_ID_PREFIX', '')
21
+ self.task_index = os.environ.get('BL_TASK_KEY', 'TASK_INDEX')
22
+ self.task_prefix = os.environ.get('BL_TASK_PREFIX', '')
23
+ self.execution_key = os.environ.get('BL_EXECUTION_KEY', 'BL_EXECUTION_ID')
24
+ self.execution_prefix = os.environ.get('BL_EXECUTION_PREFIX', '')
25
+
26
+ def format(self, record):
27
+ """
28
+ Formats the log record by converting it to a JSON object with trace context and environment variables.
29
+ """
30
+ log_entry = {
31
+ 'message': record.getMessage(),
32
+ 'severity': record.levelname,
33
+ self.labels_name: {}
34
+ }
35
+
36
+ # Add trace context if available
37
+ current_span = trace.get_current_span()
38
+ if current_span.is_recording():
39
+ span_context = current_span.get_span_context()
40
+ log_entry[self.trace_id_name] = f"{self.trace_id_prefix}{span_context.trace_id}"
41
+ log_entry[self.span_id_name] = f"{self.span_id_prefix}{span_context.span_id}"
42
+
43
+ # Add task ID if available
44
+ task_id = os.environ.get(self.task_index)
45
+ if task_id:
46
+ log_entry[self.labels_name]['blaxel-task'] = f"{self.task_prefix}{task_id}"
47
+
48
+ # Add execution ID if available
49
+ execution_id = os.environ.get(self.execution_key)
50
+ if execution_id:
51
+ log_entry[self.labels_name]['blaxel-execution'] = f"{self.execution_prefix}{execution_id.split('-')[-1]}"
52
+
53
+ return json.dumps(log_entry)
7
54
 
8
55
 
9
56
  class ColoredFormatter(logging.Formatter):
@@ -51,5 +98,10 @@ def init_logger(log_level: str):
51
98
  logging.getLogger('urllib3').setLevel(logging.CRITICAL)
52
99
  logging.getLogger("httpx").setLevel(logging.CRITICAL)
53
100
  handler = logging.StreamHandler()
54
- handler.setFormatter(ColoredFormatter("%(levelname)s %(name)s - %(message)s"))
101
+
102
+ logger_type = os.environ.get("BL_LOGGER", "http")
103
+ if logger_type == "json":
104
+ handler.setFormatter(JsonFormatter())
105
+ else:
106
+ handler.setFormatter(ColoredFormatter("%(levelname)s %(name)s - %(message)s"))
55
107
  logging.basicConfig(level=log_level, handlers=[handler])
blaxel/common/settings.py CHANGED
@@ -62,9 +62,14 @@ class Settings:
62
62
  @property
63
63
  def run_internal_hostname(self) -> str:
64
64
  """Get the run internal hostname."""
65
- return ''
66
- # Not working in python
67
- # 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", "")
68
73
 
69
74
  @property
70
75
  def bl_cloud(self) -> bool:
@@ -49,7 +49,6 @@ class AsyncLogRecordProcessor(LogRecordProcessor):
49
49
 
50
50
  def emit(self, log_data: LogData):
51
51
  if self._shutdown:
52
- _logger.warning("Processor is already shutdown, ignoring call")
53
52
  return
54
53
  self._queue.put(log_data)
55
54
 
@@ -7,6 +7,7 @@ import importlib
7
7
  import logging
8
8
  import signal
9
9
  import time
10
+ import os
10
11
  from typing import Any, Dict, List, Optional, Type
11
12
 
12
13
  from opentelemetry import metrics, trace
@@ -164,14 +165,15 @@ class TelemetryManager:
164
165
  metrics.set_meter_provider(meter_provider)
165
166
  self.meter = meter_provider.get_meter(__name__)
166
167
 
167
- # Set up the LoggerProvider
168
- self.logger_provider = LoggerProvider(resource=resource)
169
- set_logger_provider(self.logger_provider)
170
- self.logger_provider.add_log_record_processor(
171
- AsyncLogRecordProcessor(self.get_log_exporter())
172
- )
173
- handler = LoggingHandler(level=logging.NOTSET, logger_provider=self.logger_provider)
174
- logging.getLogger().addHandler(handler)
168
+ logger_type = os.environ.get("BL_LOGGER", "http")
169
+ if logger_type == "http":
170
+ self.logger_provider = LoggerProvider(resource=resource)
171
+ set_logger_provider(self.logger_provider)
172
+ self.logger_provider.add_log_record_processor(
173
+ AsyncLogRecordProcessor(self.get_log_exporter())
174
+ )
175
+ handler = LoggingHandler(level=logging.NOTSET, logger_provider=self.logger_provider)
176
+ logging.getLogger().addHandler(handler)
175
177
 
176
178
  # Load and enable instrumentations
177
179
  for name, mapping in MAPPINGS.items():
@@ -0,0 +1,188 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import asyncio
5
+ import requests
6
+
7
+
8
+ from typing import Any, Dict, Callable, Awaitable
9
+ from logging import getLogger
10
+ from ..client import client
11
+ from ..common.env import env
12
+ from ..common.internal import get_global_unique_hash
13
+ from ..common.settings import settings
14
+ from ..instrumentation.span import SpanManager
15
+
16
+
17
+
18
+ class BlJobWrapper:
19
+ def get_arguments(self) -> Dict[str, Any]:
20
+ if not os.getenv('BL_EXECUTION_DATA_URL'):
21
+ parser = argparse.ArgumentParser()
22
+ # Parse known args, ignore unknown
23
+ args, unknown = parser.parse_known_args()
24
+ # Convert to dict and include unknown args
25
+ args_dict = vars(args)
26
+ # Add unknown args to dict
27
+ for i in range(0, len(unknown), 2):
28
+ if i + 1 < len(unknown):
29
+ key = unknown[i].lstrip('-')
30
+ args_dict[key] = unknown[i + 1]
31
+ return args_dict
32
+
33
+ response = requests.get(os.getenv('BL_EXECUTION_DATA_URL'))
34
+ data = response.json()
35
+ tasks = data.get('tasks', [])
36
+ return tasks[self.index] if self.index < len(tasks) else {}
37
+
38
+ @property
39
+ def index_key(self) -> str:
40
+ return os.getenv('BL_TASK_KEY', 'TASK_INDEX')
41
+
42
+ @property
43
+ def index(self) -> int:
44
+ index_value = os.getenv(self.index_key)
45
+ return int(index_value) if index_value else 0
46
+
47
+ def start(self, func: Callable):
48
+ """
49
+ Run a job defined in a function, it's run in the current process.
50
+ Handles both async and sync functions.
51
+ Arguments are passed as keyword arguments to the function.
52
+ """
53
+ attributes = {
54
+ "span.type": "job.start",
55
+ }
56
+ try:
57
+ parsed_args = self.get_arguments()
58
+ if asyncio.iscoroutinefunction(func):
59
+ asyncio.run(func(**parsed_args))
60
+ else:
61
+ func(**parsed_args)
62
+ except Exception as error:
63
+ print('Job execution failed:', error, file=sys.stderr)
64
+ sys.exit(1)
65
+
66
+
67
+
68
+
69
+ logger = getLogger(__name__)
70
+
71
+ class BlJob:
72
+ def __init__(self, name: str):
73
+ self.name = name
74
+
75
+ @property
76
+ def internal_url(self):
77
+ """Get the internal URL for the job using a hash of workspace and job name."""
78
+ hash = get_global_unique_hash(settings.workspace, "job", self.name)
79
+ return f"{settings.run_internal_protocol}://bl-{settings.env}-{hash}.{settings.run_internal_hostname}"
80
+
81
+ @property
82
+ def forced_url(self):
83
+ """Get the forced URL from environment variables if set."""
84
+ env_var = self.name.replace("-", "_").upper()
85
+ if env[f"BL_JOB_{env_var}_URL"]:
86
+ return env[f"BL_JOB_{env_var}_URL"]
87
+ return None
88
+
89
+ @property
90
+ def external_url(self):
91
+ return f"{settings.run_url}/{settings.workspace}/jobs/{self.name}"
92
+
93
+ @property
94
+ def fallback_url(self):
95
+ if self.external_url != self.url:
96
+ return self.external_url
97
+ return None
98
+
99
+ @property
100
+ def url(self):
101
+ if self.forced_url:
102
+ return self.forced_url
103
+ if settings.run_internal_hostname:
104
+ return self.internal_url
105
+ return self.external_url
106
+
107
+ def call(self, url, input_data, headers: dict = {}, params: dict = {}):
108
+ body = {
109
+ "tasks": input_data
110
+ }
111
+
112
+ return client.get_httpx_client().post(
113
+ url+"/executions",
114
+ headers={
115
+ 'Content-Type': 'application/json',
116
+ **headers
117
+ },
118
+ json=body,
119
+ params=params
120
+ )
121
+
122
+ async def acall(self, url, input_data, headers: dict = {}, params: dict = {}):
123
+ logger.debug(f"Job Calling: {self.name}")
124
+ body = {
125
+ "tasks": input_data
126
+ }
127
+
128
+ return await client.get_async_httpx_client().post(
129
+ url+"/executions",
130
+ headers={
131
+ 'Content-Type': 'application/json',
132
+ **headers
133
+ },
134
+ json=body,
135
+ params=params
136
+ )
137
+
138
+ def run(self, input: Any, headers: dict = {}, params: dict = {}) -> str:
139
+ attributes = {
140
+ "job.name": self.name,
141
+ "span.type": "job.run",
142
+ }
143
+ with SpanManager("blaxel-tracer").create_active_span(self.name, attributes) as span:
144
+ logger.debug(f"Job Calling: {self.name}")
145
+ response = self.call(self.url, input, headers, params)
146
+ if response.status_code >= 400:
147
+ if not self.fallback_url:
148
+ span.set_attribute("job.run.error", response.text)
149
+ raise Exception(f"Job {self.name} returned status code {response.status_code} with body {response.text}")
150
+ response = self.call(self.fallback_url, input, headers, params)
151
+ if response.status_code >= 400:
152
+ span.set_attribute("job.run.error", response.text)
153
+ raise Exception(f"Job {self.name} returned status code {response.status_code} with body {response.text}")
154
+ span.set_attribute("job.run.result", response.text)
155
+ return response.text
156
+
157
+ async def arun(self, input: Any, headers: dict = {}, params: dict = {}) -> Awaitable[str]:
158
+ attributes = {
159
+ "job.name": self.name,
160
+ "span.type": "job.run",
161
+ }
162
+ with SpanManager("blaxel-tracer").create_active_span(self.name, attributes) as span:
163
+ logger.debug(f"Job Calling: {self.name}")
164
+ response = await self.acall(self.url, input, headers, params)
165
+ if response.status_code >= 400:
166
+ if not self.fallback_url:
167
+ span.set_attribute("job.run.error", response.text)
168
+ raise Exception(f"Job {self.name} returned status code {response.status_code} with body {response.text}")
169
+ response = await self.acall(self.fallback_url, input, headers, params)
170
+ if response.status_code >= 400:
171
+ span.set_attribute("job.run.error", response.text)
172
+ raise Exception(f"Job {self.name} returned status code {response.status_code} with body {response.text}")
173
+ span.set_attribute("job.run.result", response.text)
174
+ return response.text
175
+
176
+ def __str__(self):
177
+ return f"Job {self.name}"
178
+
179
+ def __repr__(self):
180
+ return self.__str__()
181
+
182
+
183
+
184
+ def bl_job(name: str):
185
+ return BlJob(name)
186
+
187
+ # Create a singleton instance
188
+ bl_start_job = BlJobWrapper()
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blaxel
3
- Version: 0.1.13
3
+ Version: 0.1.14
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
266
- blaxel/common/logger.py,sha256=emqgonfZMBIaQPowpngWOOZxWQieKP-yj_OzGZT8Oe8,1918
267
- blaxel/common/settings.py,sha256=uVM6pjcovw_JPjiNZQb9WbtvLu3RdVGy7PNBcDGzoWY,2190
265
+ blaxel/common/internal.py,sha256=PExgeKfJEmjINKreNb3r2nB5GAfG7uJhbfqHxuxBED8,2395
266
+ blaxel/common/logger.py,sha256=7oWvrZ4fg7qUfrXe7oFAeH-1pTxadrvWQmPDn7bvbmQ,4111
267
+ blaxel/common/settings.py,sha256=7KTryuBdud0IfHqykX7xEEtpgq5M5h1Z8YEzYKsHB-Q,2327
268
268
  blaxel/instrumentation/exporters.py,sha256=EoX3uaBVku1Rg49pSNXKFyHhgY5OV3Ih6UlqgjF5epw,1670
269
- blaxel/instrumentation/log.py,sha256=4tGyvLg6r4DbjqJfajYbbZ1toUzF4Q4H7kHVqYWFAEA,2537
270
- blaxel/instrumentation/manager.py,sha256=jVwVasyMI6tu0zv27DVYOaN57nXYuHFJ8QzJQKaNoK4,8880
269
+ blaxel/instrumentation/log.py,sha256=RvQByRjZMoP_dRaAZu8oK6DTegsHs-xV4W-UIqis6CA,2461
270
+ blaxel/instrumentation/manager.py,sha256=vX8RT84upjzgCUeiULp9QpDSSNVnPNFxLq0sMVz4Pjs,8974
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=QBStD3VbrcI93KOHLAz85XcM6L_fSmw76UyQzg50aN0,6545
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.13.dist-info/METADATA,sha256=Tj-4s4B9NAhuurkfhmtgcPorlt3y7b-UrNO4DAMPhPE,11772
344
- blaxel-0.1.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
345
- blaxel-0.1.13.dist-info/licenses/LICENSE,sha256=p5PNQvpvyDT_0aYBDgmV1fFI_vAD2aSV0wWG7VTgRis,1069
346
- blaxel-0.1.13.dist-info/RECORD,,
344
+ blaxel-0.1.14.dist-info/METADATA,sha256=GMPhdsq3mM6QaPIdKep-syYpAkoAVh4vSEkUsXBj9hM,11772
345
+ blaxel-0.1.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
346
+ blaxel-0.1.14.dist-info/licenses/LICENSE,sha256=p5PNQvpvyDT_0aYBDgmV1fFI_vAD2aSV0wWG7VTgRis,1069
347
+ blaxel-0.1.14.dist-info/RECORD,,