dreadnode 1.13.1__tar.gz → 1.13.3__tar.gz
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.
- {dreadnode-1.13.1 → dreadnode-1.13.3}/PKG-INFO +1 -1
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/api/client.py +2 -8
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/artifact/storage.py +35 -40
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/constants.py +1 -2
- dreadnode-1.13.3/dreadnode/credential_manager.py +129 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/main.py +14 -69
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/tracing/span.py +27 -43
- {dreadnode-1.13.1 → dreadnode-1.13.3}/pyproject.toml +2 -2
- dreadnode-1.13.1/dreadnode/storage_utils.py +0 -37
- {dreadnode-1.13.1 → dreadnode-1.13.3}/README.md +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/__main__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/api/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/api/models.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/api/util.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/artifact/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/artifact/merger.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/artifact/tree_builder.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/api.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/github.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/main.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/profile/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/profile/cli.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/config.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/convert.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/audio.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/base.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/image.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/object_3d.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/table.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/text.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/video.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/integrations/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/integrations/transformers.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/lookup.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/metric.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/object.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/py.typed +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/classification.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/consistency.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/contains.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/format.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/harm.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/judge.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/length.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/lexical.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/operators.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/pii.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/readability.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/rigging.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/sentiment.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/similarity.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/util.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/serialization.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/task.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/tracing/__init__.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/tracing/constants.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/tracing/exporters.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/types.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/util.py +0 -0
- {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/version.py +0 -0
|
@@ -37,7 +37,6 @@ from dreadnode.api.util import (
|
|
|
37
37
|
process_task,
|
|
38
38
|
)
|
|
39
39
|
from dreadnode.constants import (
|
|
40
|
-
DEFAULT_FS_CREDENTIAL_DURATION,
|
|
41
40
|
DEFAULT_MAX_POLL_TIME,
|
|
42
41
|
DEFAULT_POLL_INTERVAL,
|
|
43
42
|
)
|
|
@@ -521,17 +520,12 @@ class ApiClient:
|
|
|
521
520
|
|
|
522
521
|
# User data access
|
|
523
522
|
|
|
524
|
-
def get_user_data_credentials(
|
|
525
|
-
self, duration: int = DEFAULT_FS_CREDENTIAL_DURATION
|
|
526
|
-
) -> UserDataCredentials:
|
|
523
|
+
def get_user_data_credentials(self) -> UserDataCredentials:
|
|
527
524
|
"""
|
|
528
525
|
Retrieves user data credentials for secondary storage access.
|
|
529
526
|
|
|
530
|
-
Args:
|
|
531
|
-
duration: Credential lifetime in seconds (default: 4 hours)
|
|
532
|
-
|
|
533
527
|
Returns:
|
|
534
528
|
The user data credentials object.
|
|
535
529
|
"""
|
|
536
|
-
response = self._request("GET", "/user-data/credentials"
|
|
530
|
+
response = self._request("GET", "/user-data/credentials")
|
|
537
531
|
return UserDataCredentials(**response.json())
|
|
@@ -4,12 +4,9 @@ Provides efficient uploading of files and directories with deduplication.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import hashlib
|
|
7
|
-
import typing as t
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from dreadnode.storage_utils import with_credential_refresh
|
|
9
|
+
from dreadnode.credential_manager import CredentialManager
|
|
13
10
|
from dreadnode.util import logger
|
|
14
11
|
|
|
15
12
|
CHUNK_SIZE = 8 * 1024 * 1024 # 8MB
|
|
@@ -24,27 +21,15 @@ class ArtifactStorage:
|
|
|
24
21
|
- Batch uploads for directories handled by fsspec
|
|
25
22
|
"""
|
|
26
23
|
|
|
27
|
-
def __init__(
|
|
28
|
-
self,
|
|
29
|
-
file_system: fsspec.AbstractFileSystem,
|
|
30
|
-
credential_refresher: t.Callable[[], bool] | None = None,
|
|
31
|
-
):
|
|
24
|
+
def __init__(self, credential_manager: CredentialManager):
|
|
32
25
|
"""
|
|
33
|
-
Initialize artifact storage with
|
|
26
|
+
Initialize artifact storage with credential manager.
|
|
34
27
|
|
|
35
28
|
Args:
|
|
36
|
-
|
|
37
|
-
credential_refresher: Optional function to refresh credentials when it's about to expire
|
|
29
|
+
credential_manager: Optional credential manager for S3 operations
|
|
38
30
|
"""
|
|
39
|
-
self.
|
|
40
|
-
self._credential_refresher = credential_refresher
|
|
41
|
-
|
|
42
|
-
def _refresh_credentials_if_needed(self) -> None:
|
|
43
|
-
"""Refresh credentials if refresher is available."""
|
|
44
|
-
if self._credential_refresher:
|
|
45
|
-
self._credential_refresher()
|
|
31
|
+
self._credential_manager: CredentialManager = credential_manager
|
|
46
32
|
|
|
47
|
-
@with_credential_refresh
|
|
48
33
|
def store_file(self, file_path: Path, target_key: str) -> str:
|
|
49
34
|
"""
|
|
50
35
|
Store a file in the storage system, using multipart upload for large files.
|
|
@@ -56,13 +41,19 @@ class ArtifactStorage:
|
|
|
56
41
|
Returns:
|
|
57
42
|
Full URI with protocol to the stored file
|
|
58
43
|
"""
|
|
59
|
-
if not self._file_system.exists(target_key):
|
|
60
|
-
self._file_system.put(str(file_path), target_key)
|
|
61
|
-
logger.debug("Artifact successfully stored at %s", target_key)
|
|
62
|
-
else:
|
|
63
|
-
logger.debug("Artifact already exists at %s, skipping upload.", target_key)
|
|
64
44
|
|
|
65
|
-
|
|
45
|
+
def store_operation() -> str:
|
|
46
|
+
filesystem = self._credential_manager.get_filesystem()
|
|
47
|
+
|
|
48
|
+
if not filesystem.exists(target_key):
|
|
49
|
+
filesystem.put(str(file_path), target_key)
|
|
50
|
+
logger.info("Artifact successfully stored at %s", target_key)
|
|
51
|
+
else:
|
|
52
|
+
logger.info("Artifact already exists at %s, skipping upload.", target_key)
|
|
53
|
+
|
|
54
|
+
return str(filesystem.unstrip_protocol(target_key))
|
|
55
|
+
|
|
56
|
+
return self._credential_manager.execute_with_retry(store_operation)
|
|
66
57
|
|
|
67
58
|
def batch_upload_files(self, source_paths: list[str], target_paths: list[str]) -> list[str]:
|
|
68
59
|
"""
|
|
@@ -78,23 +69,26 @@ class ArtifactStorage:
|
|
|
78
69
|
if not source_paths:
|
|
79
70
|
return []
|
|
80
71
|
|
|
81
|
-
|
|
72
|
+
def batch_upload_operation() -> list[str]:
|
|
73
|
+
filesystem = self._credential_manager.get_filesystem()
|
|
82
74
|
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
srcs = []
|
|
76
|
+
dsts = []
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
for src, dst in zip(source_paths, target_paths, strict=False):
|
|
79
|
+
if not filesystem.exists(dst):
|
|
80
|
+
srcs.append(src)
|
|
81
|
+
dsts.append(dst)
|
|
90
82
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
if srcs:
|
|
84
|
+
filesystem.put(srcs, dsts)
|
|
85
|
+
logger.info("Batch upload completed for %d files", len(srcs))
|
|
86
|
+
else:
|
|
87
|
+
logger.info("All files already exist, skipping upload")
|
|
96
88
|
|
|
97
|
-
|
|
89
|
+
return [str(filesystem.unstrip_protocol(target)) for target in target_paths]
|
|
90
|
+
|
|
91
|
+
return self._credential_manager.execute_with_retry(batch_upload_operation)
|
|
98
92
|
|
|
99
93
|
def compute_file_hash(self, file_path: Path, stream_threshold_mb: int = 10) -> str:
|
|
100
94
|
"""
|
|
@@ -107,8 +101,9 @@ class ArtifactStorage:
|
|
|
107
101
|
Returns:
|
|
108
102
|
First 16 chars of SHA1 hash
|
|
109
103
|
"""
|
|
104
|
+
|
|
110
105
|
file_size = file_path.stat().st_size
|
|
111
|
-
stream_threshold = stream_threshold_mb * 1024 * 1024
|
|
106
|
+
stream_threshold = stream_threshold_mb * 1024 * 1024
|
|
112
107
|
|
|
113
108
|
sha1 = hashlib.sha1() # noqa: S324 # nosec
|
|
114
109
|
|
|
@@ -58,5 +58,4 @@ USER_CONFIG_PATH = pathlib.Path(
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
# Default values for the file system credential management
|
|
61
|
-
|
|
62
|
-
FS_CREDENTIAL_REFRESH_BUFFER = 300 # 5 minutes in seconds
|
|
61
|
+
FS_CREDENTIAL_REFRESH_BUFFER = 900 # 15 minutes in seconds
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
5
|
+
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
from s3fs import S3FileSystem # type: ignore[import-untyped]
|
|
8
|
+
|
|
9
|
+
from dreadnode.constants import FS_CREDENTIAL_REFRESH_BUFFER
|
|
10
|
+
from dreadnode.util import logger, resolve_endpoint
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from dreadnode.api.models import UserDataCredentials
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CredentialManager:
|
|
20
|
+
"""Simple credential manager that handles S3 credential refresh automatically."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, credential_fetcher: Callable[[], "UserDataCredentials"]):
|
|
23
|
+
"""
|
|
24
|
+
Initialize credential manager.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
credential_fetcher: Function that returns new UserDataCredentials when called
|
|
28
|
+
"""
|
|
29
|
+
self._credential_fetcher = credential_fetcher
|
|
30
|
+
self._credentials: UserDataCredentials | None = None
|
|
31
|
+
self._credentials_expiry: datetime | None = None
|
|
32
|
+
self._filesystem: S3FileSystem | None = None
|
|
33
|
+
self._prefix = ""
|
|
34
|
+
|
|
35
|
+
def initialize(self) -> None:
|
|
36
|
+
"""Initialize with fresh credentials."""
|
|
37
|
+
self._refresh_credentials()
|
|
38
|
+
|
|
39
|
+
def get_filesystem(self) -> S3FileSystem:
|
|
40
|
+
"""Get current filesystem, refreshing credentials if needed."""
|
|
41
|
+
if self._needs_refresh():
|
|
42
|
+
self._refresh_credentials()
|
|
43
|
+
assert self._filesystem is not None # noqa: S101
|
|
44
|
+
return self._filesystem
|
|
45
|
+
|
|
46
|
+
def get_prefix(self) -> str:
|
|
47
|
+
"""Get current prefix path."""
|
|
48
|
+
return self._prefix
|
|
49
|
+
|
|
50
|
+
def _needs_refresh(self) -> bool:
|
|
51
|
+
"""Check if credentials need refreshing."""
|
|
52
|
+
if not self._credentials_expiry or not self._filesystem:
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
now = datetime.now(timezone.utc)
|
|
56
|
+
time_left = (self._credentials_expiry - now).total_seconds()
|
|
57
|
+
return time_left < FS_CREDENTIAL_REFRESH_BUFFER
|
|
58
|
+
|
|
59
|
+
def _refresh_credentials(self) -> None:
|
|
60
|
+
"""Refresh credentials and create new filesystem."""
|
|
61
|
+
try:
|
|
62
|
+
logger.info("Refreshing storage credentials")
|
|
63
|
+
new_credentials = self._credential_fetcher()
|
|
64
|
+
resolved_endpoint = resolve_endpoint(new_credentials.endpoint)
|
|
65
|
+
|
|
66
|
+
new_filesystem = S3FileSystem(
|
|
67
|
+
key=new_credentials.access_key_id,
|
|
68
|
+
secret=new_credentials.secret_access_key,
|
|
69
|
+
token=new_credentials.session_token,
|
|
70
|
+
client_kwargs={
|
|
71
|
+
"endpoint_url": resolved_endpoint,
|
|
72
|
+
"region_name": new_credentials.region,
|
|
73
|
+
},
|
|
74
|
+
use_listings_cache=False,
|
|
75
|
+
listings_expiry_time=0,
|
|
76
|
+
skip_instance_cache=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Update internal state
|
|
80
|
+
self._credentials = new_credentials
|
|
81
|
+
self._credentials_expiry = new_credentials.expiration
|
|
82
|
+
self._filesystem = new_filesystem
|
|
83
|
+
self._prefix = f"{new_credentials.bucket}/{new_credentials.prefix}/"
|
|
84
|
+
|
|
85
|
+
logger.info("Storage credentials refreshed, valid until %s", self._credentials_expiry)
|
|
86
|
+
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.exception("Failed to refresh storage credentials")
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
def execute_with_retry(self, operation: Callable[[], T], max_retries: int = 3) -> T:
|
|
92
|
+
"""
|
|
93
|
+
Execute an operation with automatic credential refresh on auth errors.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
operation: Function to execute (should use self.get_filesystem())
|
|
97
|
+
max_retries: Maximum number of retry attempts
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Result of the operation
|
|
101
|
+
"""
|
|
102
|
+
for attempt in range(max_retries):
|
|
103
|
+
try:
|
|
104
|
+
return operation()
|
|
105
|
+
except ClientError as e: # noqa: PERF203
|
|
106
|
+
error_code = e.response.get("Error", {}).get("Code", "")
|
|
107
|
+
if error_code in ["ExpiredToken", "InvalidAccessKeyId", "SignatureDoesNotMatch"]:
|
|
108
|
+
logger.info(
|
|
109
|
+
"Credential error on attempt %d/%d, refreshing...", attempt + 1, max_retries
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
self._refresh_credentials()
|
|
114
|
+
except Exception:
|
|
115
|
+
logger.exception("Failed to refresh credentials")
|
|
116
|
+
if attempt == max_retries - 1:
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
if attempt < max_retries - 1:
|
|
120
|
+
time.sleep(attempt + 1)
|
|
121
|
+
continue
|
|
122
|
+
else:
|
|
123
|
+
raise
|
|
124
|
+
except Exception:
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
f"Operation failed after {max_retries} attempts due to credential issues"
|
|
129
|
+
)
|
|
@@ -21,12 +21,10 @@ from opentelemetry import propagate
|
|
|
21
21
|
from opentelemetry.exporter.otlp.proto.http import Compression
|
|
22
22
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
23
23
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
24
|
-
from s3fs import S3FileSystem # type: ignore [import-untyped]
|
|
25
24
|
|
|
26
25
|
from dreadnode.api.client import ApiClient
|
|
27
26
|
from dreadnode.config import UserConfig
|
|
28
27
|
from dreadnode.constants import (
|
|
29
|
-
DEFAULT_FS_CREDENTIAL_DURATION,
|
|
30
28
|
DEFAULT_SERVER_URL,
|
|
31
29
|
ENV_API_KEY,
|
|
32
30
|
ENV_API_TOKEN,
|
|
@@ -36,8 +34,8 @@ from dreadnode.constants import (
|
|
|
36
34
|
ENV_PROJECT,
|
|
37
35
|
ENV_SERVER,
|
|
38
36
|
ENV_SERVER_URL,
|
|
39
|
-
FS_CREDENTIAL_REFRESH_BUFFER,
|
|
40
37
|
)
|
|
38
|
+
from dreadnode.credential_manager import CredentialManager
|
|
41
39
|
from dreadnode.metric import (
|
|
42
40
|
Metric,
|
|
43
41
|
MetricAggMode,
|
|
@@ -66,7 +64,7 @@ from dreadnode.types import (
|
|
|
66
64
|
Inherited,
|
|
67
65
|
JsonValue,
|
|
68
66
|
)
|
|
69
|
-
from dreadnode.util import clean_str, handle_internal_errors
|
|
67
|
+
from dreadnode.util import clean_str, handle_internal_errors
|
|
70
68
|
from dreadnode.version import VERSION
|
|
71
69
|
|
|
72
70
|
if t.TYPE_CHECKING:
|
|
@@ -75,8 +73,6 @@ if t.TYPE_CHECKING:
|
|
|
75
73
|
from opentelemetry.sdk.trace import SpanProcessor
|
|
76
74
|
from opentelemetry.trace import Tracer
|
|
77
75
|
|
|
78
|
-
from dreadnode.api.models import UserDataCredentials
|
|
79
|
-
|
|
80
76
|
|
|
81
77
|
ToObject = t.Literal["task-or-run", "run"]
|
|
82
78
|
|
|
@@ -133,7 +129,7 @@ class Dreadnode:
|
|
|
133
129
|
self.otel_scope = otel_scope
|
|
134
130
|
|
|
135
131
|
self._api: ApiClient | None = None
|
|
136
|
-
|
|
132
|
+
self._credential_manager: CredentialManager | None = None
|
|
137
133
|
self._logfire = logfire.DEFAULT_LOGFIRE_INSTANCE
|
|
138
134
|
self._logfire.config.ignore_no_config = True
|
|
139
135
|
|
|
@@ -141,8 +137,6 @@ class Dreadnode:
|
|
|
141
137
|
self._fs_prefix: str = ".dreadnode/storage/"
|
|
142
138
|
|
|
143
139
|
self._initialized = False
|
|
144
|
-
self._credentials: UserDataCredentials | None = None
|
|
145
|
-
self._credentials_expiry: datetime | None = None
|
|
146
140
|
|
|
147
141
|
def _get_profile_server(self, profile: str | None = None) -> str | None:
|
|
148
142
|
with contextlib.suppress(Exception):
|
|
@@ -353,21 +347,15 @@ class Dreadnode:
|
|
|
353
347
|
# )
|
|
354
348
|
# )
|
|
355
349
|
# )
|
|
356
|
-
self.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
client_kwargs={
|
|
366
|
-
"endpoint_url": resolved_endpoint,
|
|
367
|
-
"region_name": self._credentials.region,
|
|
368
|
-
},
|
|
369
|
-
)
|
|
370
|
-
self._fs_prefix = f"{self._credentials.bucket}/{self._credentials.prefix}/"
|
|
350
|
+
if self._api is not None:
|
|
351
|
+
api = self._api
|
|
352
|
+
self._credential_manager = CredentialManager(
|
|
353
|
+
credential_fetcher=lambda: api.get_user_data_credentials()
|
|
354
|
+
)
|
|
355
|
+
self._credential_manager.initialize()
|
|
356
|
+
|
|
357
|
+
self._fs = self._credential_manager.get_filesystem()
|
|
358
|
+
self._fs_prefix = self._credential_manager.get_prefix()
|
|
371
359
|
|
|
372
360
|
self._logfire = logfire.configure(
|
|
373
361
|
local=not self.is_default,
|
|
@@ -414,45 +402,6 @@ class Dreadnode:
|
|
|
414
402
|
|
|
415
403
|
return self._api
|
|
416
404
|
|
|
417
|
-
def _refresh_storage_credentials(self) -> bool:
|
|
418
|
-
"""Refresh storage credentials if they are about to expire."""
|
|
419
|
-
if not self._api or not self._credentials:
|
|
420
|
-
return False
|
|
421
|
-
|
|
422
|
-
now = datetime.now(timezone.utc)
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
self._credentials_expiry is None
|
|
426
|
-
or (self._credentials_expiry - now).total_seconds() < FS_CREDENTIAL_REFRESH_BUFFER
|
|
427
|
-
):
|
|
428
|
-
try:
|
|
429
|
-
logger.info("Refreshing storage credentials")
|
|
430
|
-
self._credentials = self._api.get_user_data_credentials(
|
|
431
|
-
duration=DEFAULT_FS_CREDENTIAL_DURATION
|
|
432
|
-
)
|
|
433
|
-
self._credentials_expiry = self._credentials.expiration
|
|
434
|
-
|
|
435
|
-
resolved_endpoint = resolve_endpoint(self._credentials.endpoint)
|
|
436
|
-
self._fs = S3FileSystem(
|
|
437
|
-
key=self._credentials.access_key_id,
|
|
438
|
-
secret=self._credentials.secret_access_key,
|
|
439
|
-
token=self._credentials.session_token,
|
|
440
|
-
client_kwargs={
|
|
441
|
-
"endpoint_url": resolved_endpoint,
|
|
442
|
-
"region_name": self._credentials.region,
|
|
443
|
-
},
|
|
444
|
-
)
|
|
445
|
-
logger.info(
|
|
446
|
-
f"Storage credentials refreshed, valid until {self._credentials_expiry}"
|
|
447
|
-
)
|
|
448
|
-
return True # noqa: TRY300
|
|
449
|
-
|
|
450
|
-
except Exception as e: # noqa: BLE001
|
|
451
|
-
logger.error(f"Failed to refresh storage credentials: {e}")
|
|
452
|
-
return False
|
|
453
|
-
|
|
454
|
-
return True
|
|
455
|
-
|
|
456
405
|
def _get_tracer(self, *, is_span_tracer: bool = True) -> "Tracer":
|
|
457
406
|
return self._logfire._tracer_provider.get_tracer( # noqa: SLF001
|
|
458
407
|
self.otel_scope,
|
|
@@ -822,10 +771,8 @@ class Dreadnode:
|
|
|
822
771
|
tracer=self._get_tracer(),
|
|
823
772
|
params=params,
|
|
824
773
|
tags=tags,
|
|
825
|
-
|
|
826
|
-
prefix_path=self._fs_prefix,
|
|
774
|
+
credential_manager=self._credential_manager, # type: ignore[arg-type]
|
|
827
775
|
autolog=autolog,
|
|
828
|
-
credential_refresher=self._refresh_storage_credentials if self._credentials else None,
|
|
829
776
|
)
|
|
830
777
|
|
|
831
778
|
def get_run_context(self) -> RunContext:
|
|
@@ -870,9 +817,7 @@ class Dreadnode:
|
|
|
870
817
|
return RunSpan.from_context(
|
|
871
818
|
context=run_context,
|
|
872
819
|
tracer=self._get_tracer(),
|
|
873
|
-
|
|
874
|
-
prefix_path=self._fs_prefix,
|
|
875
|
-
credential_refresher=self._refresh_storage_credentials if self._credentials else None,
|
|
820
|
+
credential_manager=self._credential_manager, # type: ignore[arg-type]
|
|
876
821
|
)
|
|
877
822
|
|
|
878
823
|
def tag(self, *tag: str, to: ToObject = "task-or-run") -> None:
|
|
@@ -9,7 +9,6 @@ from datetime import datetime, timezone
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
import typing_extensions as te
|
|
12
|
-
from fsspec import AbstractFileSystem # type: ignore [import-untyped]
|
|
13
12
|
from logfire._internal.json_encoder import logfire_json_dumps as json_dumps
|
|
14
13
|
from logfire._internal.json_schema import (
|
|
15
14
|
JsonSchemaProperties,
|
|
@@ -33,10 +32,10 @@ from dreadnode.artifact.storage import ArtifactStorage
|
|
|
33
32
|
from dreadnode.artifact.tree_builder import ArtifactTreeBuilder, DirectoryNode
|
|
34
33
|
from dreadnode.constants import DEFAULT_MAX_INLINE_OBJECT_BYTES
|
|
35
34
|
from dreadnode.convert import run_span_to_graph
|
|
35
|
+
from dreadnode.credential_manager import CredentialManager
|
|
36
36
|
from dreadnode.metric import Metric, MetricAggMode, MetricsDict
|
|
37
37
|
from dreadnode.object import Object, ObjectRef, ObjectUri, ObjectVal
|
|
38
38
|
from dreadnode.serialization import Serialized, serialize
|
|
39
|
-
from dreadnode.storage_utils import with_credential_refresh
|
|
40
39
|
from dreadnode.tracing.constants import (
|
|
41
40
|
EVENT_ATTRIBUTE_LINK_HASH,
|
|
42
41
|
EVENT_ATTRIBUTE_OBJECT_HASH,
|
|
@@ -73,6 +72,7 @@ from dreadnode.version import VERSION
|
|
|
73
72
|
if t.TYPE_CHECKING:
|
|
74
73
|
import networkx as nx # type: ignore [import-untyped]
|
|
75
74
|
|
|
75
|
+
|
|
76
76
|
logger = logging.getLogger(__name__)
|
|
77
77
|
|
|
78
78
|
R = t.TypeVar("R")
|
|
@@ -355,8 +355,7 @@ class RunSpan(Span):
|
|
|
355
355
|
name: str,
|
|
356
356
|
project: str,
|
|
357
357
|
tracer: Tracer,
|
|
358
|
-
|
|
359
|
-
prefix_path: str,
|
|
358
|
+
credential_manager: CredentialManager,
|
|
360
359
|
*,
|
|
361
360
|
attributes: AnyDict | None = None,
|
|
362
361
|
params: AnyDict | None = None,
|
|
@@ -366,7 +365,6 @@ class RunSpan(Span):
|
|
|
366
365
|
update_frequency: int = 5,
|
|
367
366
|
run_id: str | ULID | None = None,
|
|
368
367
|
type: SpanType = "run",
|
|
369
|
-
credential_refresher: t.Callable[[], bool] | None = None,
|
|
370
368
|
) -> None:
|
|
371
369
|
self.autolog = autolog
|
|
372
370
|
self.project = project
|
|
@@ -377,14 +375,16 @@ class RunSpan(Span):
|
|
|
377
375
|
self._object_schemas: dict[str, JsonDict] = {}
|
|
378
376
|
self._inputs: list[ObjectRef] = []
|
|
379
377
|
self._outputs: list[ObjectRef] = []
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
378
|
+
|
|
379
|
+
# Credential manager for S3 operations
|
|
380
|
+
self._credential_manager = credential_manager
|
|
381
|
+
|
|
382
|
+
# Initialize artifact components
|
|
383
|
+
self._artifact_storage = ArtifactStorage(credential_manager=credential_manager)
|
|
383
384
|
self._artifacts: list[DirectoryNode] = []
|
|
384
385
|
self._artifact_merger = ArtifactMerger()
|
|
385
386
|
self._artifact_tree_builder = ArtifactTreeBuilder(
|
|
386
|
-
storage=self._artifact_storage,
|
|
387
|
-
prefix_path=prefix_path,
|
|
387
|
+
storage=self._artifact_storage, prefix_path=self._credential_manager.get_prefix()
|
|
388
388
|
)
|
|
389
389
|
|
|
390
390
|
# Update mechanics
|
|
@@ -397,12 +397,9 @@ class RunSpan(Span):
|
|
|
397
397
|
self._pending_objects = deepcopy(self._objects)
|
|
398
398
|
self._pending_object_schemas = deepcopy(self._object_schemas)
|
|
399
399
|
|
|
400
|
-
self._context_token: Token[RunSpan | None] | None = None
|
|
401
|
-
self._remote_context: dict[str, str] | None = None
|
|
400
|
+
self._context_token: Token[RunSpan | None] | None = None
|
|
401
|
+
self._remote_context: dict[str, str] | None = None
|
|
402
402
|
self._remote_token: object | None = None
|
|
403
|
-
self._file_system = file_system
|
|
404
|
-
self._prefix_path = prefix_path
|
|
405
|
-
|
|
406
403
|
self._tasks: list[TaskSpan[t.Any]] = []
|
|
407
404
|
|
|
408
405
|
attributes = {
|
|
@@ -410,7 +407,7 @@ class RunSpan(Span):
|
|
|
410
407
|
SPAN_ATTRIBUTE_PROJECT: project,
|
|
411
408
|
**(attributes or {}),
|
|
412
409
|
}
|
|
413
|
-
|
|
410
|
+
|
|
414
411
|
super().__init__(name, tracer, attributes=attributes, type=type, tags=tags)
|
|
415
412
|
|
|
416
413
|
@classmethod
|
|
@@ -418,24 +415,19 @@ class RunSpan(Span):
|
|
|
418
415
|
cls,
|
|
419
416
|
context: RunContext,
|
|
420
417
|
tracer: Tracer,
|
|
421
|
-
|
|
422
|
-
prefix_path: str,
|
|
423
|
-
credential_refresher: t.Callable[[], bool] | None = None,
|
|
418
|
+
credential_manager: CredentialManager,
|
|
424
419
|
) -> "RunSpan":
|
|
425
420
|
self = RunSpan(
|
|
426
421
|
name=f"run.{context['run_id']}.fragment",
|
|
427
422
|
project=context["project"],
|
|
428
423
|
attributes={},
|
|
429
424
|
tracer=tracer,
|
|
430
|
-
file_system=file_system,
|
|
431
|
-
prefix_path=prefix_path,
|
|
432
425
|
type="run_fragment",
|
|
433
426
|
run_id=context["run_id"],
|
|
434
|
-
|
|
427
|
+
credential_manager=credential_manager,
|
|
435
428
|
)
|
|
436
429
|
|
|
437
430
|
self._remote_context = context["trace_context"]
|
|
438
|
-
|
|
439
431
|
return self
|
|
440
432
|
|
|
441
433
|
def __enter__(self) -> te.Self:
|
|
@@ -507,10 +499,6 @@ class RunSpan(Span):
|
|
|
507
499
|
if self._context_token is not None:
|
|
508
500
|
current_run_span.reset(self._context_token)
|
|
509
501
|
|
|
510
|
-
def _refresh_credentials_if_needed(self) -> None:
|
|
511
|
-
if self._credential_refresher:
|
|
512
|
-
self._credential_refresher()
|
|
513
|
-
|
|
514
502
|
def push_update(self, *, force: bool = False) -> None:
|
|
515
503
|
if self._span is None:
|
|
516
504
|
return
|
|
@@ -615,24 +603,19 @@ class RunSpan(Span):
|
|
|
615
603
|
|
|
616
604
|
return composite_hash
|
|
617
605
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
"""
|
|
621
|
-
Writes data to the given full_path in the object store if it doesn't already exist.
|
|
606
|
+
def _store_file_by_hash(self, data_bytes: bytes, full_path: str) -> str:
|
|
607
|
+
"""Store file with automatic credential refresh."""
|
|
622
608
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
full_path: The path in the object store (e.g., S3 key or local path).
|
|
609
|
+
def store_operation() -> str:
|
|
610
|
+
filesystem = self._credential_manager.get_filesystem()
|
|
626
611
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
with self._file_system.open(full_path, "wb") as f:
|
|
633
|
-
f.write(data)
|
|
612
|
+
if not filesystem.exists(full_path):
|
|
613
|
+
with filesystem.open(full_path, "wb") as f:
|
|
614
|
+
f.write(data_bytes)
|
|
615
|
+
|
|
616
|
+
return str(filesystem.unstrip_protocol(full_path))
|
|
634
617
|
|
|
635
|
-
return
|
|
618
|
+
return self._credential_manager.execute_with_retry(store_operation)
|
|
636
619
|
|
|
637
620
|
def _create_object_by_hash(self, serialized: Serialized, object_hash: str) -> Object:
|
|
638
621
|
"""Create an ObjectVal or ObjectUri depending on size with a specific hash."""
|
|
@@ -652,7 +635,8 @@ class RunSpan(Span):
|
|
|
652
635
|
# Offload to file system (e.g., S3)
|
|
653
636
|
# For storage efficiency, still use just the data_hash for the file path
|
|
654
637
|
# This ensures we don't duplicate storage for the same data
|
|
655
|
-
|
|
638
|
+
prefix = self._credential_manager.get_prefix()
|
|
639
|
+
full_path = f"{prefix.rstrip('/')}/{data_hash}"
|
|
656
640
|
object_uri = self._store_file_by_hash(data_bytes, full_path)
|
|
657
641
|
|
|
658
642
|
return ObjectUri(
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dreadnode"
|
|
3
|
-
version = "1.13.
|
|
3
|
+
version = "1.13.3"
|
|
4
4
|
description = "Dreadnode SDK"
|
|
5
5
|
requires-python = ">=3.10,<3.14"
|
|
6
6
|
|
|
7
7
|
[tool.poetry]
|
|
8
8
|
name = "dreadnode"
|
|
9
|
-
version = "1.13.
|
|
9
|
+
version = "1.13.3"
|
|
10
10
|
description = "Dreadnode SDK"
|
|
11
11
|
authors = ["Nick Landers <monoxgas@gmail.com>"]
|
|
12
12
|
repository = "https://github.com/dreadnode/sdk"
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import typing as t
|
|
3
|
-
|
|
4
|
-
from dreadnode.util import logger
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def with_credential_refresh(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
8
|
-
"""Decorator that automatically handles credential refresh on storage errors."""
|
|
9
|
-
|
|
10
|
-
@functools.wraps(func)
|
|
11
|
-
def wrapper(self: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
12
|
-
# Try to refresh credentials before operation
|
|
13
|
-
if hasattr(self, "_refresh_credentials_if_needed"):
|
|
14
|
-
self._refresh_credentials_if_needed()
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
return func(self, *args, **kwargs)
|
|
18
|
-
except Exception as e:
|
|
19
|
-
error_str = str(e)
|
|
20
|
-
if any(
|
|
21
|
-
error in error_str
|
|
22
|
-
for error in [
|
|
23
|
-
"ExpiredToken",
|
|
24
|
-
"TokenRefreshRequired",
|
|
25
|
-
"InvalidAccessKeyId",
|
|
26
|
-
"The Access Key Id you provided does not exist",
|
|
27
|
-
]
|
|
28
|
-
):
|
|
29
|
-
logger.info("Storage credential error, forcing refresh and retrying")
|
|
30
|
-
|
|
31
|
-
if hasattr(self, "_refresh_credentials_if_needed"):
|
|
32
|
-
self._refresh_credentials_if_needed()
|
|
33
|
-
|
|
34
|
-
return func(self, *args, **kwargs)
|
|
35
|
-
raise
|
|
36
|
-
|
|
37
|
-
return wrapper
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|