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.
Files changed (64) hide show
  1. {dreadnode-1.13.1 → dreadnode-1.13.3}/PKG-INFO +1 -1
  2. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/api/client.py +2 -8
  3. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/artifact/storage.py +35 -40
  4. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/constants.py +1 -2
  5. dreadnode-1.13.3/dreadnode/credential_manager.py +129 -0
  6. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/main.py +14 -69
  7. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/tracing/span.py +27 -43
  8. {dreadnode-1.13.1 → dreadnode-1.13.3}/pyproject.toml +2 -2
  9. dreadnode-1.13.1/dreadnode/storage_utils.py +0 -37
  10. {dreadnode-1.13.1 → dreadnode-1.13.3}/README.md +0 -0
  11. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/__init__.py +0 -0
  12. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/__main__.py +0 -0
  13. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/api/__init__.py +0 -0
  14. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/api/models.py +0 -0
  15. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/api/util.py +0 -0
  16. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/artifact/__init__.py +0 -0
  17. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/artifact/merger.py +0 -0
  18. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/artifact/tree_builder.py +0 -0
  19. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/__init__.py +0 -0
  20. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/api.py +0 -0
  21. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/github.py +0 -0
  22. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/main.py +0 -0
  23. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/profile/__init__.py +0 -0
  24. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/cli/profile/cli.py +0 -0
  25. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/config.py +0 -0
  26. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/convert.py +0 -0
  27. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/__init__.py +0 -0
  28. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/audio.py +0 -0
  29. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/base.py +0 -0
  30. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/image.py +0 -0
  31. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/object_3d.py +0 -0
  32. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/table.py +0 -0
  33. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/text.py +0 -0
  34. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/data_types/video.py +0 -0
  35. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/integrations/__init__.py +0 -0
  36. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/integrations/transformers.py +0 -0
  37. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/lookup.py +0 -0
  38. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/metric.py +0 -0
  39. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/object.py +0 -0
  40. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/py.typed +0 -0
  41. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/__init__.py +0 -0
  42. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/classification.py +0 -0
  43. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/consistency.py +0 -0
  44. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/contains.py +0 -0
  45. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/format.py +0 -0
  46. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/harm.py +0 -0
  47. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/judge.py +0 -0
  48. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/length.py +0 -0
  49. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/lexical.py +0 -0
  50. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/operators.py +0 -0
  51. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/pii.py +0 -0
  52. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/readability.py +0 -0
  53. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/rigging.py +0 -0
  54. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/sentiment.py +0 -0
  55. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/similarity.py +0 -0
  56. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/scorers/util.py +0 -0
  57. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/serialization.py +0 -0
  58. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/task.py +0 -0
  59. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/tracing/__init__.py +0 -0
  60. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/tracing/constants.py +0 -0
  61. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/tracing/exporters.py +0 -0
  62. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/types.py +0 -0
  63. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/util.py +0 -0
  64. {dreadnode-1.13.1 → dreadnode-1.13.3}/dreadnode/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dreadnode
3
- Version: 1.13.1
3
+ Version: 1.13.3
4
4
  Summary: Dreadnode SDK
5
5
  Author: Nick Landers
6
6
  Author-email: monoxgas@gmail.com
@@ -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", params={"duration": duration})
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
- import fsspec # type: ignore[import-untyped]
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 a file system and prefix path.
26
+ Initialize artifact storage with credential manager.
34
27
 
35
28
  Args:
36
- file_system: FSSpec-compatible file system
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._file_system = file_system
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
- return str(self._file_system.unstrip_protocol(target_key))
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
- logger.debug("Batch uploading %d files", len(source_paths))
72
+ def batch_upload_operation() -> list[str]:
73
+ filesystem = self._credential_manager.get_filesystem()
82
74
 
83
- srcs = []
84
- dsts = []
75
+ srcs = []
76
+ dsts = []
85
77
 
86
- for src, dst in zip(source_paths, target_paths, strict=False):
87
- if not self._file_system.exists(dst):
88
- srcs.append(src)
89
- dsts.append(dst)
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
- if srcs:
92
- self._file_system.put(srcs, dsts)
93
- logger.debug("Batch upload completed for %d files", len(srcs))
94
- else:
95
- logger.debug("All files already exist, skipping upload")
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
- return [str(self._file_system.unstrip_protocol(target)) for target in target_paths]
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 # Convert MB to bytes
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
- DEFAULT_FS_CREDENTIAL_DURATION = 14400 # 4 hours in seconds
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, logger, resolve_endpoint
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._credentials = self._api.get_user_data_credentials(
357
- duration=DEFAULT_FS_CREDENTIAL_DURATION
358
- )
359
- self._credentials_expiry = self._credentials.expiration
360
- resolved_endpoint = resolve_endpoint(self._credentials.endpoint)
361
- self._fs = S3FileSystem(
362
- key=self._credentials.access_key_id,
363
- secret=self._credentials.secret_access_key,
364
- token=self._credentials.session_token,
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
- file_system=self._fs,
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
- file_system=self._fs,
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
- file_system: AbstractFileSystem,
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
- self._artifact_storage = ArtifactStorage(
381
- file_system=file_system, credential_refresher=credential_refresher
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 # contextvars context
401
- self._remote_context: dict[str, str] | None = None # remote run trace context
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
- self._credential_refresher = credential_refresher
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
- file_system: AbstractFileSystem,
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
- credential_refresher=credential_refresher,
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
- @with_credential_refresh
619
- def _store_file_by_hash(self, data: bytes, full_path: str) -> str:
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
- Args:
624
- data: Content to write.
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
- Returns:
628
- The unstrip_protocol version of the full path (for object store URI).
629
- """
630
- if not self._file_system.exists(full_path):
631
- logger.debug("Storing new object at: %s", full_path)
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 str(self._file_system.unstrip_protocol(full_path))
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
- full_path = f"{self._prefix_path.rstrip('/')}/{data_hash}"
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.1"
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.0"
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