litdata 0.2.71__tar.gz → 0.2.72__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 (83) hide show
  1. {litdata-0.2.71/src/litdata.egg-info → litdata-0.2.72}/PKG-INFO +2 -1
  2. {litdata-0.2.71 → litdata-0.2.72}/requirements.txt +1 -0
  3. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/__about__.py +1 -1
  4. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/client.py +190 -35
  5. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/serializers.py +2 -2
  6. {litdata-0.2.71 → litdata-0.2.72/src/litdata.egg-info}/PKG-INFO +2 -1
  7. {litdata-0.2.71 → litdata-0.2.72}/src/litdata.egg-info/requires.txt +1 -0
  8. {litdata-0.2.71 → litdata-0.2.72}/CONTRIBUTING.md +0 -0
  9. {litdata-0.2.71 → litdata-0.2.72}/LICENSE +0 -0
  10. {litdata-0.2.71 → litdata-0.2.72}/MANIFEST.in +0 -0
  11. {litdata-0.2.71 → litdata-0.2.72}/README.md +0 -0
  12. {litdata-0.2.71 → litdata-0.2.72}/setup.cfg +0 -0
  13. {litdata-0.2.71 → litdata-0.2.72}/setup.py +0 -0
  14. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/__init__.py +0 -0
  15. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/__main__.py +0 -0
  16. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/cli/__init__.py +0 -0
  17. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/cli/commands.py +0 -0
  18. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/cli/handler/__init__.py +0 -0
  19. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/cli/handler/cache.py +0 -0
  20. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/cli/handler/optimize.py +0 -0
  21. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/cli/parser.py +0 -0
  22. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/constants.py +0 -0
  23. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/debugger.py +0 -0
  24. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/exceptions.py +0 -0
  25. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/helpers.py +0 -0
  26. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/imports.py +0 -0
  27. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/processing/__init__.py +0 -0
  28. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/processing/complete.py +0 -0
  29. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/processing/data_processor.py +0 -0
  30. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/processing/functions.py +0 -0
  31. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/processing/media_folder.py +0 -0
  32. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/processing/readers.py +0 -0
  33. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/processing/utilities.py +0 -0
  34. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/raw/__init__.py +0 -0
  35. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/raw/dataset.py +0 -0
  36. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/raw/indexer.py +0 -0
  37. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/raw/types.py +0 -0
  38. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/requirements.py +0 -0
  39. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/__init__.py +0 -0
  40. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/async_prefetch.py +0 -0
  41. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/cache.py +0 -0
  42. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/collate.py +0 -0
  43. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/combined.py +0 -0
  44. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/compression.py +0 -0
  45. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/config.py +0 -0
  46. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/dataloader.py +0 -0
  47. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/dataset.py +0 -0
  48. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/dataset_update.py +0 -0
  49. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/downloader.py +0 -0
  50. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/elastic.py +0 -0
  51. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/fs_provider.py +0 -0
  52. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/item_loader.py +0 -0
  53. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/parallel.py +0 -0
  54. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/posix_fast.py +0 -0
  55. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/reader.py +0 -0
  56. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/resolver.py +0 -0
  57. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/sampler.py +0 -0
  58. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/shuffle.py +0 -0
  59. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/timing.py +0 -0
  60. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/streaming/writer.py +0 -0
  61. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/types.py +0 -0
  62. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/__init__.py +0 -0
  63. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/_pytree.py +0 -0
  64. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/base.py +0 -0
  65. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/breakpoint.py +0 -0
  66. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/broadcast.py +0 -0
  67. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/dataset_utilities.py +0 -0
  68. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/encryption.py +0 -0
  69. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/env.py +0 -0
  70. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/format.py +0 -0
  71. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/hf_dataset.py +0 -0
  72. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/keys_index.py +0 -0
  73. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/packing.py +0 -0
  74. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/parquet.py +0 -0
  75. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/shuffle.py +0 -0
  76. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/subsample.py +0 -0
  77. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/torch_utils.py +0 -0
  78. {litdata-0.2.71 → litdata-0.2.72}/src/litdata/utilities/train_test_split.py +0 -0
  79. {litdata-0.2.71 → litdata-0.2.72}/src/litdata.egg-info/SOURCES.txt +0 -0
  80. {litdata-0.2.71 → litdata-0.2.72}/src/litdata.egg-info/dependency_links.txt +0 -0
  81. {litdata-0.2.71 → litdata-0.2.72}/src/litdata.egg-info/entry_points.txt +0 -0
  82. {litdata-0.2.71 → litdata-0.2.72}/src/litdata.egg-info/not-zip-safe +0 -0
  83. {litdata-0.2.71 → litdata-0.2.72}/src/litdata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: litdata
3
- Version: 0.2.71
3
+ Version: 0.2.72
4
4
  Summary: The Deep Learning framework to train, deploy, and ship AI products Lightning fast.
5
5
  Home-page: https://github.com/Lightning-AI/litdata
6
6
  Download-URL: https://github.com/Lightning-AI/litdata
@@ -34,6 +34,7 @@ Requires-Dist: filelock
34
34
  Requires-Dist: numpy
35
35
  Requires-Dist: boto3
36
36
  Requires-Dist: requests
37
+ Requires-Dist: urllib3>=1.26
37
38
  Requires-Dist: tifffile
38
39
  Requires-Dist: obstore
39
40
  Provides-Extra: extras
@@ -5,5 +5,6 @@ filelock
5
5
  numpy
6
6
  boto3
7
7
  requests
8
+ urllib3 >=1.26 # Retry(allowed_methods=...) in streaming/client.py
8
9
  tifffile
9
10
  obstore
@@ -14,7 +14,7 @@
14
14
 
15
15
  import time
16
16
 
17
- __version__ = "0.2.71"
17
+ __version__ = "0.2.72"
18
18
  __author__ = "Lightning AI et al."
19
19
  __author_email__ = "pytorch@lightning.ai"
20
20
  __license__ = "Apache-2.0"
@@ -12,9 +12,11 @@
12
12
  # limitations under the License.
13
13
 
14
14
  import json
15
+ import logging
15
16
  import os
17
+ import random
16
18
  import threading
17
- from time import time
19
+ from time import sleep, time
18
20
  from typing import Any
19
21
 
20
22
  import boto3
@@ -27,14 +29,60 @@ from urllib3.util.retry import Retry
27
29
 
28
30
  from litdata.constants import _IS_IN_STUDIO
29
31
 
32
+ logger = logging.getLogger("litdata.streaming.client")
33
+
30
34
  # Constants for the retry adapter. Docs: https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html
31
- # Maximum number of total connection retry attempts (e.g., 2880 retries = 24 hours with 30s timeout per request)
32
- _CONNECTION_RETRY_TOTAL = 2880
35
+ # Retries per request. Deliberately small: a refresh runs inline on whichever DataLoader worker
36
+ # thread crossed the interval and holds the client lock for its whole duration, so it has to
37
+ # return in seconds, not hours. `S3Client._refresh_client` is what rides out a longer outage.
38
+ _CONNECTION_RETRY_TOTAL = 4
33
39
  # Backoff factor for connection retries (wait time increases by this factor after each failure)
34
40
  _CONNECTION_RETRY_BACKOFF_FACTOR = 0.5
35
41
  # Default timeout for each HTTP request in seconds
36
42
  _DEFAULT_REQUEST_TIMEOUT = 30 # seconds
37
43
 
44
+ # The control plane mints credentials with a 1 hour TTL for S3 project-role data connections, and
45
+ # the response carries no expiry for us to read, so refresh well inside it. The remaining time is
46
+ # the window in which a failed refresh can be retried while the credentials in hand still work.
47
+ _DEFAULT_REFETCH_INTERVAL = 2700 # seconds
48
+ # How long past the refetch interval we keep serving existing credentials while refreshes fail.
49
+ # Sized against the TTL, not comfort: 2700 + 600 leaves ~5 minutes before the 1 hour S3 expiry,
50
+ # so we stop before reads start failing as unexplained S3 403s.
51
+ _REFRESH_GRACE_PERIOD = 600 # seconds
52
+ # How long to wait for the control plane when there are no credentials yet. No TTL constrains
53
+ # this one — nothing is being served — so it can be more patient than the refresh grace.
54
+ _INITIAL_RETRY_BUDGET = 900 # seconds
55
+ # Spacing between refresh attempts once one has failed. Without it every subsequent read
56
+ # re-requests credentials, turning one control-plane blip into a request storm per worker.
57
+ _REFRESH_RETRY_INTERVAL = 60 # seconds
58
+ # Fraction of the interval by which each process refreshes early. DataLoader workers are forked
59
+ # together, so without jitter they all reach the interval — and stampede — in the same instant.
60
+ _REFETCH_JITTER_RATIO = 0.1
61
+
62
+
63
+ class _CredentialsError(RuntimeError):
64
+ """Raised when credentials could not be obtained from the control plane or IMDS.
65
+
66
+ Only these are retried. Anything else out of ``_create_client`` — a bad ``storage_options``
67
+ key, a malformed endpoint — is a local mistake that no amount of waiting fixes, and must
68
+ reach the caller immediately rather than stalling the job for the retry budget.
69
+ """
70
+
71
+
72
+ class _CredentialsUnavailableError(_CredentialsError):
73
+ """A credential failure that may clear on its own: unreachable, timed out, 5xx, 429."""
74
+
75
+
76
+ class _CredentialsConfigurationError(_CredentialsError):
77
+ """A credential failure that retrying cannot fix: missing configuration, or rejected auth."""
78
+
79
+
80
+ def _credentials_error(status_code: int, message: str) -> _CredentialsError:
81
+ """Classify an HTTP failure. A 4xx other than 408/429 will not fix itself on a retry."""
82
+ if 400 <= status_code < 500 and status_code not in (408, 429):
83
+ return _CredentialsConfigurationError(message)
84
+ return _CredentialsUnavailableError(message)
85
+
38
86
 
39
87
  class _CustomRetryAdapter(HTTPAdapter):
40
88
  def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -42,7 +90,11 @@ class _CustomRetryAdapter(HTTPAdapter):
42
90
  super().__init__(*args, **kwargs)
43
91
 
44
92
  def send(self, request: Any, *args: Any, **kwargs: Any) -> Any:
45
- kwargs["timeout"] = kwargs.get("timeout", self.timeout)
93
+ # requests always passes `timeout` explicitly, as None when the caller gave none, so a
94
+ # `kwargs.get("timeout", self.timeout)` default never applied and every request without
95
+ # an explicit timeout could hang forever.
96
+ if kwargs.get("timeout") is None:
97
+ kwargs["timeout"] = self.timeout
46
98
  return super().send(request, **kwargs)
47
99
 
48
100
 
@@ -57,6 +109,10 @@ def _login_and_get_temp_bucket_credentials(data_connection_id: str) -> dict[str,
57
109
  retry_strategy = Retry(
58
110
  total=_CONNECTION_RETRY_TOTAL,
59
111
  backoff_factor=_CONNECTION_RETRY_BACKOFF_FACTOR,
112
+ # urllib3 leaves POST out of its default allowed_methods, which left the login below
113
+ # unretried: a single 429 or 502 there failed the whole refresh even though the
114
+ # credentials GET would have retried it. Minting a token is safe to repeat.
115
+ allowed_methods=Retry.DEFAULT_ALLOWED_METHODS | {"POST"},
60
116
  status_forcelist=[
61
117
  408, # Request Timeout
62
118
  429, # Too Many Requests
@@ -77,17 +133,27 @@ def _login_and_get_temp_bucket_credentials(data_connection_id: str) -> dict[str,
77
133
  project_id = os.getenv("LIGHTNING_CLOUD_PROJECT_ID")
78
134
 
79
135
  if not all([api_key, username, project_id]):
80
- raise RuntimeError("Missing required environment variables")
136
+ raise _CredentialsConfigurationError("Missing required environment variables")
81
137
 
82
138
  # Login to get token
83
139
  payload = {"apiKey": api_key, "username": username}
84
140
  login_url = f"{cloud_url}/v1/auth/login"
85
- response = session.post(login_url, data=json.dumps(payload))
86
-
87
- if "token" not in response.json():
88
- raise RuntimeError("Failed to get authentication token")
141
+ try:
142
+ response = session.post(login_url, data=json.dumps(payload))
143
+ except requests.exceptions.RequestException as e:
144
+ raise _CredentialsUnavailableError(f"Could not reach the Lightning Cloud API to log in: {e}") from e
145
+
146
+ # Check the status before the body: a proxy in front of the API answers with HTML, which
147
+ # would otherwise surface as a JSONDecodeError with nothing pointing back at the login call.
148
+ if response.status_code != 200:
149
+ raise _credentials_error(
150
+ response.status_code, f"Failed to log in to the Lightning Cloud API: {response.status_code}"
151
+ )
89
152
 
90
- token = response.json()["token"]
153
+ try:
154
+ token = response.json()["token"]
155
+ except (ValueError, KeyError) as e:
156
+ raise RuntimeError("Failed to get authentication token") from e
91
157
 
92
158
  # Get temporary bucket credentials
93
159
  headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
@@ -95,10 +161,15 @@ def _login_and_get_temp_bucket_credentials(data_connection_id: str) -> dict[str,
95
161
  f"{cloud_url}/v1/projects/{project_id}/data-connections/{data_connection_id}/temp-bucket-credentials"
96
162
  )
97
163
 
98
- credentials_response = session.get(credentials_url, headers=headers, timeout=10)
164
+ try:
165
+ credentials_response = session.get(credentials_url, headers=headers, timeout=10)
166
+ except requests.exceptions.RequestException as e:
167
+ raise _CredentialsUnavailableError(f"Could not reach the Lightning Cloud API for credentials: {e}") from e
99
168
 
100
169
  if credentials_response.status_code != 200:
101
- raise RuntimeError(f"Failed to get credentials: {credentials_response.status_code}")
170
+ raise _credentials_error(
171
+ credentials_response.status_code, f"Failed to get credentials: {credentials_response.status_code}"
172
+ )
102
173
 
103
174
  return credentials_response.json()
104
175
 
@@ -108,17 +179,33 @@ class S3Client:
108
179
 
109
180
  def __init__(
110
181
  self,
111
- refetch_interval: int = 3300,
182
+ refetch_interval: int = _DEFAULT_REFETCH_INTERVAL,
112
183
  storage_options: dict | None = {},
113
184
  session_options: dict | None = {},
114
185
  ) -> None:
115
186
  self._refetch_interval = refetch_interval
116
187
  self._last_time: float | None = None
117
- self._client: Any | None = None
118
188
  self._storage_options: dict = storage_options or {}
119
189
  self._session_options: dict = session_options or {}
190
+ self._reset_process_state()
191
+
192
+ def _reset_process_state(self) -> None:
193
+ """Reset the state that must not be shared across a fork.
194
+
195
+ Re-rolls the refresh jitter too: DataLoader workers inherit the parent's timings, so
196
+ without a fresh roll per process they would all refresh in the same instant.
197
+ """
198
+ self._client: Any | None = None
120
199
  # Guards lazy create + credential refresh (range GETs hit .client from many threads).
121
200
  self._client_lock = threading.Lock()
201
+ self._owner_pid = os.getpid()
202
+ self._refetch_deadline = self._jittered_refetch_interval()
203
+ self._refresh_retry_time: float | None = None
204
+
205
+ def _jittered_refetch_interval(self) -> float:
206
+ # Only ever early, never late: callers set the interval as an upper bound on how long
207
+ # a set of credentials is held, and the grace period below is measured against it.
208
+ return self._refetch_interval * (1.0 - random.uniform(0.0, _REFETCH_JITTER_RATIO)) # noqa: S311
122
209
 
123
210
  def __getstate__(self) -> dict[str, Any]:
124
211
  state = self.__dict__.copy()
@@ -130,9 +217,7 @@ class S3Client:
130
217
 
131
218
  def __setstate__(self, state: dict[str, Any]) -> None:
132
219
  self.__dict__.update(state)
133
- self._client = None
134
- self._client_lock = threading.Lock()
135
- self._owner_pid = os.getpid()
220
+ self._reset_process_state()
136
221
 
137
222
  def _create_client(self) -> None:
138
223
  # S3 data connections marked available on non-AWS providers can't reach the bucket via the
@@ -159,7 +244,10 @@ class S3Client:
159
244
  )
160
245
  else:
161
246
  provider = InstanceMetadataProvider(iam_role_fetcher=InstanceMetadataFetcher(timeout=3600, num_attempts=5))
162
- credentials = provider.load()
247
+ try:
248
+ credentials = provider.load()
249
+ except Exception as e:
250
+ raise _CredentialsUnavailableError(f"Could not load instance metadata credentials: {e}") from e
163
251
  session = boto3.Session()
164
252
  self._client = session.client(
165
253
  "s3",
@@ -192,24 +280,91 @@ class S3Client:
192
280
  },
193
281
  )
194
282
 
283
+ def _mark_refreshed(self) -> None:
284
+ self._last_time = time()
285
+ self._refetch_deadline = self._jittered_refetch_interval()
286
+ self._refresh_retry_time = None
287
+
288
+ def _create_initial_client(self) -> None:
289
+ """Create the first client, waiting out a control plane that is briefly unavailable.
290
+
291
+ Unlike a refresh there are no credentials to fall back on, so this has to keep trying
292
+ rather than hand the caller an error. Bounded by ``_INITIAL_RETRY_BUDGET``: a job that
293
+ cannot reach the control plane for that long should fail with a clear reason.
294
+ """
295
+ started = time()
296
+ attempt = 0
297
+ while True:
298
+ attempt += 1
299
+ try:
300
+ self._create_client()
301
+ # Only a control plane that might come back is worth waiting for. A configuration
302
+ # error, or any local failure building the client, propagates on the first attempt.
303
+ except _CredentialsUnavailableError as e:
304
+ waited = time() - started
305
+ if waited + _REFRESH_RETRY_INTERVAL >= _INITIAL_RETRY_BUDGET:
306
+ raise RuntimeError(f"Could not get credentials after {waited:.0f}s of retrying: {e}") from e
307
+ logger.warning(
308
+ "Could not get credentials (attempt %d, %.0fs elapsed), so data loading is blocked; "
309
+ "retrying in %ds: %s",
310
+ attempt,
311
+ waited,
312
+ _REFRESH_RETRY_INTERVAL,
313
+ e,
314
+ )
315
+ sleep(_REFRESH_RETRY_INTERVAL)
316
+ else:
317
+ self._mark_refreshed()
318
+ return
319
+
320
+ def _refresh_client(self) -> None:
321
+ """Re-mint credentials, tolerating a control plane that is briefly unavailable.
322
+
323
+ Credentials are refreshed before they expire, so a failed refresh is not immediately
324
+ fatal — the ones already in hand still work. Keep serving those and retry on a timer
325
+ until the grace period runs out, rather than failing the read on the first error, which
326
+ kills the DataLoader worker and with it the run.
327
+ """
328
+ now = time()
329
+ if self._refresh_retry_time is not None and now < self._refresh_retry_time:
330
+ return
331
+
332
+ try:
333
+ self._create_client()
334
+ # Both kinds get the grace period here, unlike initial creation. The credentials in hand
335
+ # still work, so there is nothing to gain by failing fast on a 403 that might be a proxy
336
+ # misbehaving mid-deploy — and if it is a real revocation, the deadline still catches it.
337
+ except _CredentialsError as e:
338
+ held_for = 0.0 if self._last_time is None else now - self._last_time
339
+ if held_for > self._refetch_deadline + _REFRESH_GRACE_PERIOD:
340
+ raise RuntimeError(
341
+ f"Failed to refresh credentials for {held_for:.0f}s, so they are assumed expired: {e}"
342
+ ) from e
343
+ self._refresh_retry_time = now + _REFRESH_RETRY_INTERVAL
344
+ logger.warning(
345
+ "Could not refresh credentials (%.0fs since the last successful refresh); reusing the current "
346
+ "ones and retrying in %ds: %s",
347
+ held_for,
348
+ _REFRESH_RETRY_INTERVAL,
349
+ e,
350
+ )
351
+ return
352
+
353
+ self._mark_refreshed()
354
+
195
355
  @property
196
356
  def client(self) -> Any:
197
357
  # boto3 clients are thread-safe for requests; construction/refresh is not.
198
- pid = os.getpid()
199
- if getattr(self, "_owner_pid", None) != pid:
200
- # DataLoader fork: drop the inherited client and lock.
201
- self._client = None
202
- self._client_lock = threading.Lock()
203
- self._owner_pid = pid
358
+ if getattr(self, "_owner_pid", None) != os.getpid():
359
+ # DataLoader fork: drop the inherited client, lock and refresh schedule.
360
+ self._reset_process_state()
361
+
204
362
  with self._client_lock:
205
363
  if self._client is None:
206
- self._create_client()
207
- self._last_time = time()
208
-
364
+ self._create_initial_client()
209
365
  # Re-generate credentials for EC2 / temporary Studio creds
210
- if self._last_time is None or (time() - self._last_time) > self._refetch_interval:
211
- self._create_client()
212
- self._last_time = time()
366
+ elif self._last_time is None or (time() - self._last_time) > self._refetch_deadline:
367
+ self._refresh_client()
213
368
 
214
369
  return self._client
215
370
 
@@ -219,7 +374,7 @@ class R2Client(S3Client):
219
374
 
220
375
  def __init__(
221
376
  self,
222
- refetch_interval: int = 3300,
377
+ refetch_interval: int = _DEFAULT_REFETCH_INTERVAL,
223
378
  storage_options: dict | None = {},
224
379
  session_options: dict | None = {},
225
380
  ) -> None:
@@ -248,17 +403,17 @@ class R2Client(S3Client):
248
403
  "endpoint_url": endpoint_url,
249
404
  }
250
405
 
406
+ except _CredentialsError:
407
+ raise
251
408
  except Exception as e:
252
- # Fallback to hardcoded credentials if API call fails
253
- print(f"Failed to get R2 credentials from API: {e}. Using fallback credentials.")
254
- raise RuntimeError(f"Failed to get R2 credentials and no fallback available: {e}")
409
+ raise RuntimeError(f"Failed to get R2 credentials: {e}") from e
255
410
 
256
411
  def _create_client(self) -> None:
257
412
  """Create a new R2 client with fresh credentials."""
258
413
  # Get data connection ID from storage options
259
414
  data_connection_id = self._base_storage_options.get("data_connection_id")
260
415
  if not data_connection_id:
261
- raise RuntimeError("data_connection_id is required in storage_options for R2 client")
416
+ raise _CredentialsConfigurationError("data_connection_id is required in storage_options for R2 client")
262
417
 
263
418
  # Get fresh R2 credentials
264
419
  r2_credentials = self.get_r2_bucket_credentials(data_connection_id)
@@ -477,7 +477,7 @@ class NumpySerializer(Serializer):
477
477
  shape.append(np.frombuffer(data[8 + 4 * shape_idx : 8 + 4 * (shape_idx + 1)], np.uint32).item())
478
478
 
479
479
  # deserialize the numpy array bytes
480
- tensor = np.frombuffer(data[8 + 4 * shape_size : len(data)], dtype=dtype)
480
+ tensor = np.frombuffer(data[8 + 4 * shape_size : len(data)], dtype=dtype).copy()
481
481
  if tensor.shape == shape:
482
482
  return tensor
483
483
  return np.reshape(tensor, shape)
@@ -503,7 +503,7 @@ class NoHeaderNumpySerializer(Serializer):
503
503
 
504
504
  def deserialize(self, data: bytes) -> np.ndarray:
505
505
  assert self._dtype
506
- return np.frombuffer(data, dtype=self._dtype)
506
+ return np.frombuffer(data, dtype=self._dtype).copy()
507
507
 
508
508
  def can_serialize(self, item: np.ndarray) -> bool:
509
509
  return isinstance(item, np.ndarray) and len(item.shape) == 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: litdata
3
- Version: 0.2.71
3
+ Version: 0.2.72
4
4
  Summary: The Deep Learning framework to train, deploy, and ship AI products Lightning fast.
5
5
  Home-page: https://github.com/Lightning-AI/litdata
6
6
  Download-URL: https://github.com/Lightning-AI/litdata
@@ -34,6 +34,7 @@ Requires-Dist: filelock
34
34
  Requires-Dist: numpy
35
35
  Requires-Dist: boto3
36
36
  Requires-Dist: requests
37
+ Requires-Dist: urllib3>=1.26
37
38
  Requires-Dist: tifffile
38
39
  Requires-Dist: obstore
39
40
  Provides-Extra: extras
@@ -5,6 +5,7 @@ filelock
5
5
  numpy
6
6
  boto3
7
7
  requests
8
+ urllib3>=1.26
8
9
  tifffile
9
10
  obstore
10
11
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes