dreadnode 1.13.0__tar.gz → 1.13.2__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 (63) hide show
  1. {dreadnode-1.13.0 → dreadnode-1.13.2}/PKG-INFO +1 -1
  2. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/api/client.py +5 -2
  3. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/artifact/storage.py +15 -1
  4. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/constants.py +4 -0
  5. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/main.py +63 -14
  6. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/serialization.py +1 -1
  7. dreadnode-1.13.2/dreadnode/storage_utils.py +37 -0
  8. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/tracing/span.py +13 -1
  9. {dreadnode-1.13.0 → dreadnode-1.13.2}/pyproject.toml +1 -1
  10. {dreadnode-1.13.0 → dreadnode-1.13.2}/README.md +0 -0
  11. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/__init__.py +0 -0
  12. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/__main__.py +0 -0
  13. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/api/__init__.py +0 -0
  14. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/api/models.py +0 -0
  15. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/api/util.py +0 -0
  16. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/artifact/__init__.py +0 -0
  17. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/artifact/merger.py +0 -0
  18. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/artifact/tree_builder.py +0 -0
  19. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/cli/__init__.py +0 -0
  20. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/cli/api.py +0 -0
  21. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/cli/github.py +0 -0
  22. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/cli/main.py +0 -0
  23. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/cli/profile/__init__.py +0 -0
  24. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/cli/profile/cli.py +0 -0
  25. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/config.py +0 -0
  26. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/convert.py +0 -0
  27. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/data_types/__init__.py +0 -0
  28. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/data_types/audio.py +0 -0
  29. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/data_types/base.py +0 -0
  30. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/data_types/image.py +0 -0
  31. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/data_types/object_3d.py +0 -0
  32. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/data_types/table.py +0 -0
  33. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/data_types/text.py +0 -0
  34. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/data_types/video.py +0 -0
  35. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/integrations/__init__.py +0 -0
  36. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/integrations/transformers.py +0 -0
  37. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/lookup.py +0 -0
  38. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/metric.py +0 -0
  39. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/object.py +0 -0
  40. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/py.typed +0 -0
  41. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/__init__.py +0 -0
  42. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/classification.py +0 -0
  43. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/consistency.py +0 -0
  44. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/contains.py +0 -0
  45. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/format.py +0 -0
  46. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/harm.py +0 -0
  47. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/judge.py +0 -0
  48. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/length.py +0 -0
  49. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/lexical.py +0 -0
  50. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/operators.py +0 -0
  51. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/pii.py +0 -0
  52. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/readability.py +0 -0
  53. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/rigging.py +0 -0
  54. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/sentiment.py +0 -0
  55. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/similarity.py +0 -0
  56. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/scorers/util.py +0 -0
  57. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/task.py +0 -0
  58. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/tracing/__init__.py +0 -0
  59. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/tracing/constants.py +0 -0
  60. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/tracing/exporters.py +0 -0
  61. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/types.py +0 -0
  62. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/util.py +0 -0
  63. {dreadnode-1.13.0 → dreadnode-1.13.2}/dreadnode/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dreadnode
3
- Version: 1.13.0
3
+ Version: 1.13.2
4
4
  Summary: Dreadnode SDK
5
5
  Author: Nick Landers
6
6
  Author-email: monoxgas@gmail.com
@@ -36,7 +36,10 @@ from dreadnode.api.util import (
36
36
  process_run,
37
37
  process_task,
38
38
  )
39
- from dreadnode.constants import DEFAULT_MAX_POLL_TIME, DEFAULT_POLL_INTERVAL
39
+ from dreadnode.constants import (
40
+ DEFAULT_MAX_POLL_TIME,
41
+ DEFAULT_POLL_INTERVAL,
42
+ )
40
43
  from dreadnode.util import logger
41
44
  from dreadnode.version import VERSION
42
45
 
@@ -524,5 +527,5 @@ class ApiClient:
524
527
  Returns:
525
528
  The user data credentials object.
526
529
  """
527
- response = self.request("GET", "/user-data/credentials")
530
+ response = self._request("GET", "/user-data/credentials")
528
531
  return UserDataCredentials(**response.json())
@@ -4,10 +4,12 @@ Provides efficient uploading of files and directories with deduplication.
4
4
  """
5
5
 
6
6
  import hashlib
7
+ import typing as t
7
8
  from pathlib import Path
8
9
 
9
10
  import fsspec # type: ignore[import-untyped]
10
11
 
12
+ from dreadnode.storage_utils import with_credential_refresh
11
13
  from dreadnode.util import logger
12
14
 
13
15
  CHUNK_SIZE = 8 * 1024 * 1024 # 8MB
@@ -22,15 +24,27 @@ class ArtifactStorage:
22
24
  - Batch uploads for directories handled by fsspec
23
25
  """
24
26
 
25
- def __init__(self, file_system: fsspec.AbstractFileSystem):
27
+ def __init__(
28
+ self,
29
+ file_system: fsspec.AbstractFileSystem,
30
+ credential_refresher: t.Callable[[], bool] | None = None,
31
+ ):
26
32
  """
27
33
  Initialize artifact storage with a file system and prefix path.
28
34
 
29
35
  Args:
30
36
  file_system: FSSpec-compatible file system
37
+ credential_refresher: Optional function to refresh credentials when it's about to expire
31
38
  """
32
39
  self._file_system = file_system
40
+ self._credential_refresher = credential_refresher
33
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()
46
+
47
+ @with_credential_refresh
34
48
  def store_file(self, file_path: Path, target_key: str) -> str:
35
49
  """
36
50
  Store a file in the storage system, using multipart upload for large files.
@@ -39,6 +39,7 @@ ENV_API_KEY = "DREADNODE_API_KEY" # pragma: allowlist secret (alternative to AP
39
39
  ENV_LOCAL_DIR = "DREADNODE_LOCAL_DIR"
40
40
  ENV_PROJECT = "DREADNODE_PROJECT"
41
41
  ENV_PROFILE = "DREADNODE_PROFILE"
42
+ ENV_CONSOLE = "DREADNODE_CONSOLE"
42
43
 
43
44
  #
44
45
  # Environment
@@ -55,3 +56,6 @@ USER_CONFIG_PATH = pathlib.Path(
55
56
  # allow overriding the user config file via env variable
56
57
  os.getenv("DREADNODE_USER_CONFIG_FILE") or pathlib.Path.home() / ".dreadnode" / "config"
57
58
  )
59
+
60
+ # Default values for the file system credential management
61
+ FS_CREDENTIAL_REFRESH_BUFFER = 300 # 5 minutes in seconds
@@ -29,11 +29,13 @@ from dreadnode.constants import (
29
29
  DEFAULT_SERVER_URL,
30
30
  ENV_API_KEY,
31
31
  ENV_API_TOKEN,
32
+ ENV_CONSOLE,
32
33
  ENV_LOCAL_DIR,
33
34
  ENV_PROFILE,
34
35
  ENV_PROJECT,
35
36
  ENV_SERVER,
36
37
  ENV_SERVER_URL,
38
+ FS_CREDENTIAL_REFRESH_BUFFER,
37
39
  )
38
40
  from dreadnode.metric import (
39
41
  Metric,
@@ -63,7 +65,7 @@ from dreadnode.types import (
63
65
  Inherited,
64
66
  JsonValue,
65
67
  )
66
- from dreadnode.util import clean_str, handle_internal_errors, resolve_endpoint
68
+ from dreadnode.util import clean_str, handle_internal_errors, logger, resolve_endpoint
67
69
  from dreadnode.version import VERSION
68
70
 
69
71
  if t.TYPE_CHECKING:
@@ -72,6 +74,8 @@ if t.TYPE_CHECKING:
72
74
  from opentelemetry.sdk.trace import SpanProcessor
73
75
  from opentelemetry.trace import Tracer
74
76
 
77
+ from dreadnode.api.models import UserDataCredentials
78
+
75
79
 
76
80
  ToObject = t.Literal["task-or-run", "run"]
77
81
 
@@ -100,7 +104,7 @@ class Dreadnode:
100
104
  project: str | None
101
105
  service_name: str | None
102
106
  service_version: str | None
103
- console: logfire.ConsoleOptions | t.Literal[False, True]
107
+ console: logfire.ConsoleOptions | bool
104
108
  send_to_logfire: bool | t.Literal["if-token-present"]
105
109
  otel_scope: str
106
110
 
@@ -113,7 +117,7 @@ class Dreadnode:
113
117
  project: str | None = None,
114
118
  service_name: str | None = None,
115
119
  service_version: str | None = None,
116
- console: logfire.ConsoleOptions | t.Literal[False, True] = True,
120
+ console: logfire.ConsoleOptions | bool = True,
117
121
  send_to_logfire: bool | t.Literal["if-token-present"] = False,
118
122
  otel_scope: str = "dreadnode",
119
123
  ) -> None:
@@ -136,6 +140,8 @@ class Dreadnode:
136
140
  self._fs_prefix: str = ".dreadnode/storage/"
137
141
 
138
142
  self._initialized = False
143
+ self._credentials: UserDataCredentials | None = None
144
+ self._credentials_expiry: datetime | None = None
139
145
 
140
146
  def _get_profile_server(self, profile: str | None = None) -> str | None:
141
147
  with contextlib.suppress(Exception):
@@ -167,7 +173,7 @@ class Dreadnode:
167
173
  project: str | None = None,
168
174
  service_name: str | None = None,
169
175
  service_version: str | None = None,
170
- console: logfire.ConsoleOptions | t.Literal[False, True] = True,
176
+ console: logfire.ConsoleOptions | bool | None = None,
171
177
  send_to_logfire: bool | t.Literal["if-token-present"] = False,
172
178
  otel_scope: str = "dreadnode",
173
179
  ) -> None:
@@ -195,7 +201,7 @@ class Dreadnode:
195
201
  project: The default project name to associate all runs with.
196
202
  service_name: The service name to use for OpenTelemetry.
197
203
  service_version: The service version to use for OpenTelemetry.
198
- console: Whether to log span information to the console.
204
+ console: Whether to log span information to the console (`DREADNODE_CONSOLE` or the default is True).
199
205
  send_to_logfire: Whether to send data to Logfire.
200
206
  otel_scope: The OpenTelemetry scope name.
201
207
  """
@@ -252,7 +258,11 @@ class Dreadnode:
252
258
  self.project = project or os.environ.get(ENV_PROJECT)
253
259
  self.service_name = service_name
254
260
  self.service_version = service_version
255
- self.console = console
261
+ self.console = console or os.environ.get(ENV_CONSOLE, "true").lower() in [
262
+ "true",
263
+ "1",
264
+ "yes",
265
+ ]
256
266
  self.send_to_logfire = send_to_logfire
257
267
  self.otel_scope = otel_scope
258
268
 
@@ -342,19 +352,19 @@ class Dreadnode:
342
352
  # )
343
353
  # )
344
354
  # )
345
-
346
- credentials = self._api.get_user_data_credentials()
347
- resolved_endpoint = resolve_endpoint(credentials.endpoint)
355
+ self._credentials = self._api.get_user_data_credentials()
356
+ self._credentials_expiry = self._credentials.expiration
357
+ resolved_endpoint = resolve_endpoint(self._credentials.endpoint)
348
358
  self._fs = S3FileSystem(
349
- key=credentials.access_key_id,
350
- secret=credentials.secret_access_key,
351
- token=credentials.session_token,
359
+ key=self._credentials.access_key_id,
360
+ secret=self._credentials.secret_access_key,
361
+ token=self._credentials.session_token,
352
362
  client_kwargs={
353
363
  "endpoint_url": resolved_endpoint,
354
- "region_name": credentials.region,
364
+ "region_name": self._credentials.region,
355
365
  },
356
366
  )
357
- self._fs_prefix = f"{credentials.bucket}/{credentials.prefix}/"
367
+ self._fs_prefix = f"{self._credentials.bucket}/{self._credentials.prefix}/"
358
368
 
359
369
  self._logfire = logfire.configure(
360
370
  local=not self.is_default,
@@ -401,6 +411,43 @@ class Dreadnode:
401
411
 
402
412
  return self._api
403
413
 
414
+ def _refresh_storage_credentials(self) -> bool:
415
+ """Refresh storage credentials if they are about to expire."""
416
+ if not self._api or not self._credentials:
417
+ return False
418
+
419
+ now = datetime.now(timezone.utc)
420
+
421
+ if (
422
+ self._credentials_expiry is None
423
+ or (self._credentials_expiry - now).total_seconds() < FS_CREDENTIAL_REFRESH_BUFFER
424
+ ):
425
+ try:
426
+ logger.info("Refreshing storage credentials")
427
+ self._credentials = self._api.get_user_data_credentials()
428
+ self._credentials_expiry = self._credentials.expiration
429
+
430
+ resolved_endpoint = resolve_endpoint(self._credentials.endpoint)
431
+ self._fs = S3FileSystem(
432
+ key=self._credentials.access_key_id,
433
+ secret=self._credentials.secret_access_key,
434
+ token=self._credentials.session_token,
435
+ client_kwargs={
436
+ "endpoint_url": resolved_endpoint,
437
+ "region_name": self._credentials.region,
438
+ },
439
+ )
440
+ logger.info(
441
+ f"Storage credentials refreshed, valid until {self._credentials_expiry}"
442
+ )
443
+ return True # noqa: TRY300
444
+
445
+ except Exception as e: # noqa: BLE001
446
+ logger.error(f"Failed to refresh storage credentials: {e}")
447
+ return False
448
+
449
+ return True
450
+
404
451
  def _get_tracer(self, *, is_span_tracer: bool = True) -> "Tracer":
405
452
  return self._logfire._tracer_provider.get_tracer( # noqa: SLF001
406
453
  self.otel_scope,
@@ -773,6 +820,7 @@ class Dreadnode:
773
820
  file_system=self._fs,
774
821
  prefix_path=self._fs_prefix,
775
822
  autolog=autolog,
823
+ credential_refresher=self._refresh_storage_credentials if self._credentials else None,
776
824
  )
777
825
 
778
826
  def get_run_context(self) -> RunContext:
@@ -819,6 +867,7 @@ class Dreadnode:
819
867
  tracer=self._get_tracer(),
820
868
  file_system=self._fs,
821
869
  prefix_path=self._fs_prefix,
870
+ credential_refresher=self._refresh_storage_credentials if self._credentials else None,
822
871
  )
823
872
 
824
873
  def tag(self, *tag: str, to: ToObject = "task-or-run") -> None:
@@ -519,7 +519,7 @@ def _serialize(obj: t.Any, seen: set[int] | None = None) -> tuple[JsonValue, Jso
519
519
  # Primitives early
520
520
 
521
521
  if isinstance(obj, str | int | float | bool) or obj is None:
522
- return obj, EMPTY_SCHEMA
522
+ return obj, {}
523
523
 
524
524
  # Cycle tracking
525
525
 
@@ -0,0 +1,37 @@
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
@@ -36,6 +36,7 @@ from dreadnode.convert import run_span_to_graph
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
39
40
  from dreadnode.tracing.constants import (
40
41
  EVENT_ATTRIBUTE_LINK_HASH,
41
42
  EVENT_ATTRIBUTE_OBJECT_HASH,
@@ -365,6 +366,7 @@ class RunSpan(Span):
365
366
  update_frequency: int = 5,
366
367
  run_id: str | ULID | None = None,
367
368
  type: SpanType = "run",
369
+ credential_refresher: t.Callable[[], bool] | None = None,
368
370
  ) -> None:
369
371
  self.autolog = autolog
370
372
  self.project = project
@@ -375,7 +377,9 @@ class RunSpan(Span):
375
377
  self._object_schemas: dict[str, JsonDict] = {}
376
378
  self._inputs: list[ObjectRef] = []
377
379
  self._outputs: list[ObjectRef] = []
378
- self._artifact_storage = ArtifactStorage(file_system=file_system)
380
+ self._artifact_storage = ArtifactStorage(
381
+ file_system=file_system, credential_refresher=credential_refresher
382
+ )
379
383
  self._artifacts: list[DirectoryNode] = []
380
384
  self._artifact_merger = ArtifactMerger()
381
385
  self._artifact_tree_builder = ArtifactTreeBuilder(
@@ -406,6 +410,7 @@ class RunSpan(Span):
406
410
  SPAN_ATTRIBUTE_PROJECT: project,
407
411
  **(attributes or {}),
408
412
  }
413
+ self._credential_refresher = credential_refresher
409
414
  super().__init__(name, tracer, attributes=attributes, type=type, tags=tags)
410
415
 
411
416
  @classmethod
@@ -415,6 +420,7 @@ class RunSpan(Span):
415
420
  tracer: Tracer,
416
421
  file_system: AbstractFileSystem,
417
422
  prefix_path: str,
423
+ credential_refresher: t.Callable[[], bool] | None = None,
418
424
  ) -> "RunSpan":
419
425
  self = RunSpan(
420
426
  name=f"run.{context['run_id']}.fragment",
@@ -425,6 +431,7 @@ class RunSpan(Span):
425
431
  prefix_path=prefix_path,
426
432
  type="run_fragment",
427
433
  run_id=context["run_id"],
434
+ credential_refresher=credential_refresher,
428
435
  )
429
436
 
430
437
  self._remote_context = context["trace_context"]
@@ -500,6 +507,10 @@ class RunSpan(Span):
500
507
  if self._context_token is not None:
501
508
  current_run_span.reset(self._context_token)
502
509
 
510
+ def _refresh_credentials_if_needed(self) -> None:
511
+ if self._credential_refresher:
512
+ self._credential_refresher()
513
+
503
514
  def push_update(self, *, force: bool = False) -> None:
504
515
  if self._span is None:
505
516
  return
@@ -604,6 +615,7 @@ class RunSpan(Span):
604
615
 
605
616
  return composite_hash
606
617
 
618
+ @with_credential_refresh
607
619
  def _store_file_by_hash(self, data: bytes, full_path: str) -> str:
608
620
  """
609
621
  Writes data to the given full_path in the object store if it doesn't already exist.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dreadnode"
3
- version = "1.13.0"
3
+ version = "1.13.2"
4
4
  description = "Dreadnode SDK"
5
5
  requires-python = ">=3.10,<3.14"
6
6
 
File without changes
File without changes
File without changes