tensorfs 0.3.27__tar.gz → 0.3.30__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.

Potentially problematic release.


This version of tensorfs might be problematic. Click here for more details.

Files changed (126) hide show
  1. {tensorfs-0.3.27 → tensorfs-0.3.30}/Cargo.lock +2 -2
  2. {tensorfs-0.3.27 → tensorfs-0.3.30}/Cargo.toml +1 -1
  3. {tensorfs-0.3.27 → tensorfs-0.3.30}/PKG-INFO +1 -1
  4. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/fetch.rs +49 -6
  5. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/main.rs +12 -1
  6. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/providers.rs +475 -1
  7. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/api.rs +7 -1
  8. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/http.rs +166 -16
  9. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/hub.rs +16 -1
  10. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/ledger.rs +104 -0
  11. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/pull.rs +46 -1
  12. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/push.rs +4 -1
  13. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/tests.rs +261 -1
  14. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/src/store.rs +24 -3
  15. {tensorfs-0.3.27 → tensorfs-0.3.30}/pyproject.toml +1 -1
  16. {tensorfs-0.3.27 → tensorfs-0.3.30}/python/tensorfs/_ext.pyi +1 -1
  17. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/Cargo.toml +0 -0
  18. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/build.rs +0 -0
  19. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  20. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  21. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  22. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  23. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/b64.rs +0 -0
  24. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  25. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  26. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  27. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  28. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  29. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  30. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  31. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/canon.rs +0 -0
  32. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/capability.rs +0 -0
  33. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/catalog.rs +0 -0
  34. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/cbor.rs +0 -0
  35. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  36. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/corpus.rs +0 -0
  37. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  38. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  39. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  40. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  41. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  42. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  43. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/derived.rs +0 -0
  44. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/dtype.rs +0 -0
  45. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/durability.rs +0 -0
  46. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/err.rs +0 -0
  47. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/fetch.rs +0 -0
  48. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/fill.rs +0 -0
  49. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/fit.rs +0 -0
  50. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/gc.rs +0 -0
  51. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/header.rs +0 -0
  52. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/home.rs +0 -0
  53. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ids.rs +0 -0
  54. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  55. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  56. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  57. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  58. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  59. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  60. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  61. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  62. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  63. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  64. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/jcs.rs +0 -0
  65. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/lib.rs +0 -0
  66. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/limits.rs +0 -0
  67. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/machine.rs +0 -0
  68. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/manifest.rs +0 -0
  69. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/meta.rs +0 -0
  70. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/project.rs +0 -0
  71. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/read.rs +0 -0
  72. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/receipt.rs +0 -0
  73. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/reclaim.rs +0 -0
  74. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/registry.rs +0 -0
  75. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/relation.rs +0 -0
  76. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  77. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/repository.rs +0 -0
  78. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/sha256.rs +0 -0
  79. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  80. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/spec.rs +0 -0
  81. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/staging.rs +0 -0
  82. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/storage.rs +0 -0
  83. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/store.rs +0 -0
  84. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  85. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  86. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  87. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  88. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/src/vectors.rs +0 -0
  89. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  90. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fit.rs +0 -0
  91. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  92. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  93. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  94. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  95. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  96. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  97. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  98. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  99. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/home.rs +0 -0
  100. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  101. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  102. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/liveness.rs +0 -0
  103. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  104. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  105. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  106. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  107. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  108. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  109. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  110. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  111. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  112. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  113. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  114. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  115. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/staging.rs +0 -0
  116. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  117. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  118. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/version.rs +0 -0
  119. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-core/tests/write_through.rs +0 -0
  120. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/Cargo.toml +0 -0
  121. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/src/docs.rs +0 -0
  122. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/src/errors.rs +0 -0
  123. {tensorfs-0.3.27 → tensorfs-0.3.30}/crates/tensorfs-py/src/lib.rs +0 -0
  124. {tensorfs-0.3.27 → tensorfs-0.3.30}/python/tensorfs/__init__.py +0 -0
  125. {tensorfs-0.3.27 → tensorfs-0.3.30}/python/tensorfs/errors.py +0 -0
  126. {tensorfs-0.3.27 → tensorfs-0.3.30}/python/tensorfs/py.typed +0 -0
@@ -392,7 +392,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
392
392
 
393
393
  [[package]]
394
394
  name = "tensorfs-core"
395
- version = "0.3.27"
395
+ version = "0.3.30"
396
396
  dependencies = [
397
397
  "fs2",
398
398
  "minicbor",
@@ -404,7 +404,7 @@ dependencies = [
404
404
 
405
405
  [[package]]
406
406
  name = "tensorfs-py"
407
- version = "0.3.27"
407
+ version = "0.3.30"
408
408
  dependencies = [
409
409
  "pyo3",
410
410
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.27"
6
+ version = "0.3.30"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.27
3
+ Version: 0.3.30
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -228,6 +228,34 @@ fn credential_of(
228
228
  /// unbound Store is a pull with no cache, and a cache that is missing, cold, corrupt,
229
229
  /// read-only or unmounted degrades to exactly that pull.
230
230
  pub fn cmd_pull(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
231
+ pull_with_budget(root, refspec, flags, None)
232
+ }
233
+
234
+ /// A distinct verb makes an older CLI refuse instead of ignoring new quota flags.
235
+ pub fn cmd_bounded_pull(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
236
+ let bound = |name: &str| {
237
+ let values = crate::flag_all(flags, name);
238
+ if values.len() != 1 {
239
+ return None;
240
+ }
241
+ values[0].parse::<u64>().ok()
242
+ };
243
+ let (Some(bytes), Some(objects)) = (bound("max-download-bytes"), bound("max-download-objects"))
244
+ else {
245
+ eprintln!(
246
+ "bounded fetch requires one unsigned --max-download-bytes and --max-download-objects"
247
+ );
248
+ return ExitCode::from(2);
249
+ };
250
+ pull_with_budget(root, refspec, flags, Some((bytes, objects)))
251
+ }
252
+
253
+ fn pull_with_budget(
254
+ root: &Path,
255
+ refspec: &str,
256
+ flags: &Flags,
257
+ budget: Option<(u64, u64)>,
258
+ ) -> ExitCode {
231
259
  let store = ok!(Store::open(root));
232
260
  let Some(base) = flag(flags, "hub") else {
233
261
  eprintln!("--hub <base-url> is required");
@@ -247,6 +275,7 @@ pub fn cmd_pull(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
247
275
  request.session = flag(flags, "session").unwrap_or("");
248
276
  request.streams = crate::flag_num(flags, "streams", transport::STREAMS);
249
277
  request.deadline = deadline_of(flags);
278
+ request.download_budget = budget;
250
279
  // THE WALK REPORTS WHILE IT WALKS.
251
280
  //
252
281
  // Until now `tfs fetch` printed one report at the end, the pod's supervisor buffered
@@ -258,17 +287,18 @@ pub fn cmd_pull(root: &Path, refspec: &str, flags: &Flags) -> ExitCode {
258
287
  // What is printed is what was MEASURED — bytes through each door and the instant they
259
288
  // landed — and nothing derived. A rate is the consumer's division of two of these
260
289
  // samples, which is the only place it can be computed honestly: this walk has sixteen
261
- // streams and no opinion about what interval anyone wants to average over. There is no
262
- // denominator here on purpose. `FetchPlan` knows it and `pull()` holds it, but a
263
- // caller that must not invent a percentage would rather be given none than one this
264
- // side guessed.
290
+ // streams and no opinion about what interval anyone wants to average over. The
291
+ // denominator is the native FetchPlan's declared byte count. The plan callback also
292
+ // supplies already-held bytes, so a resumed or warm pull starts at its real position.
265
293
  //
266
294
  // The two doors are separate counters because they cost different things and only one
267
295
  // of them is an invoice. A cache that has silently gone cold produces a complete
268
296
  // closure, correct totals, and a bill — and looks exactly like a warm one unless the
269
297
  // split is reported while it happens.
270
298
  let progress = PullProgress::new(request.sample_seconds);
299
+ let planned = |plan: &FetchPlan| progress.planned(plan);
271
300
  let observe = |_object: &ObjectRef, moved: u64, door: ObjectSource| progress.saw(moved, door);
301
+ request.on_plan = Some(&planned);
272
302
  request.on_object = Some(&observe);
273
303
  let report = ok!(transport::pull(&request));
274
304
  progress.settle();
@@ -1282,6 +1312,8 @@ struct PullProgress {
1282
1312
  }
1283
1313
 
1284
1314
  struct PullProgressState {
1315
+ total_bytes: u64,
1316
+ held_bytes: u64,
1285
1317
  origin_bytes: u64,
1286
1318
  cache_bytes: u64,
1287
1319
  origin_objects: u64,
@@ -1293,6 +1325,8 @@ impl PullProgress {
1293
1325
  fn new(sample_seconds: f64) -> PullProgress {
1294
1326
  PullProgress {
1295
1327
  state: Mutex::new(PullProgressState {
1328
+ total_bytes: 0,
1329
+ held_bytes: 0,
1296
1330
  origin_bytes: 0,
1297
1331
  cache_bytes: 0,
1298
1332
  origin_objects: 0,
@@ -1303,6 +1337,14 @@ impl PullProgress {
1303
1337
  }
1304
1338
  }
1305
1339
 
1340
+ fn planned(&self, plan: &FetchPlan) {
1341
+ let mut state = self.state.lock().unwrap();
1342
+ state.total_bytes = plan.declared_bytes();
1343
+ state.held_bytes = plan.held_bytes();
1344
+ state.last = Some(Instant::now());
1345
+ print_progress(&state);
1346
+ }
1347
+
1306
1348
  fn saw(&self, moved: u64, door: ObjectSource) {
1307
1349
  let mut state = self.state.lock().unwrap();
1308
1350
  match door {
@@ -1364,7 +1406,8 @@ impl PullProgress {
1364
1406
  /// door at all, and those are the two states worth telling apart.
1365
1407
  fn print_progress(state: &PullProgressState) {
1366
1408
  eprintln!(
1367
- "{{\"event\":\"pull.progress\",\"origin_bytes\":{},\"cache_bytes\":{},\"origin_objects\":{},\"cache_objects\":{}}}",
1368
- state.origin_bytes, state.cache_bytes, state.origin_objects, state.cache_objects
1409
+ "{{\"event\":\"pull.progress\",\"total_bytes\":{},\"held_bytes\":{},\"origin_bytes\":{},\"cache_bytes\":{},\"origin_objects\":{},\"cache_objects\":{}}}",
1410
+ state.total_bytes, state.held_bytes, state.origin_bytes, state.cache_bytes,
1411
+ state.origin_objects, state.cache_objects
1369
1412
  );
1370
1413
  }
@@ -47,6 +47,8 @@
47
47
  //! and the origin. It is bound by `tfs store ensure --repo-cache` and never by a flag here.
48
48
  //!
49
49
  //! tfs fetch <root> <ref> closure -> plan -> N-way walk (presign at use) -> proof
50
+ //! tfs fetch bounded <root> <ref> --max-download-bytes N --max-download-objects N
51
+ //! same pull, refused before body transfer if new objects exceed bounds
50
52
  //! tfs fetch plan-objects <root> <session> --refs <jsonl> HELD/WANTED, no manifest
51
53
  //! tfs fetch url <root> <sha256> <length> <url|-> [--progress] [--streams N]
52
54
  //! one granted object, in parallel ranges
@@ -351,6 +353,7 @@ fn main() -> ExitCode {
351
353
  ["fetch", "plan-objects", r, s] => fetch::cmd_plan_objects(Path::new(r), s, &flags),
352
354
  ["fetch", "admit", r, p, i] => fetch::cmd_admit(Path::new(r), Path::new(p), i, &flags),
353
355
  ["fetch", "complete", r, p] => fetch::cmd_complete(Path::new(r), Path::new(p)),
356
+ ["fetch", "bounded", r, refspec] => fetch::cmd_bounded_pull(Path::new(r), refspec, &flags),
354
357
  ["fetch", "url", r, i, l, u] => fetch::cmd_url(Path::new(r), i, l, u, &flags),
355
358
  ["staging", "fetch", r, i, l, u] => fetch::cmd_staging_fetch(Path::new(r), i, l, u, &flags),
356
359
  ["staging", "drop", r, i] => fetch::cmd_staging_drop(Path::new(r), i),
@@ -361,7 +364,15 @@ fn main() -> ExitCode {
361
364
  ["source", "plan", u] => fetch::cmd_source_plan(u, &flags),
362
365
  ["source", "pull", r, u] => fetch::cmd_source_pull(Path::new(r), u, &flags),
363
366
  ["fetch", sub, _t]
364
- if ["plan", "plan-objects", "admit", "complete", "url"].contains(sub) =>
367
+ if [
368
+ "plan",
369
+ "plan-objects",
370
+ "admit",
371
+ "complete",
372
+ "url",
373
+ "bounded",
374
+ ]
375
+ .contains(sub) =>
365
376
  {
366
377
  eprintln!("usage: see the header of {}", file!());
367
378
  ExitCode::from(2)
@@ -30,6 +30,8 @@ const API_PAGE_MAX: usize = 64;
30
30
  /// A member the provider does not identify by digest is fetched whole and hashed; the
31
31
  /// class is HuggingFace non-LFS files (configs, tokenizers), which git itself keeps small.
32
32
  const UNPINNED_MEMBER_MAX_BYTES: u64 = 64 << 20;
33
+ /// An explicit ordinary-file selection is a bounded working set, not a weight download.
34
+ const FILE_SELECTION_MAX_BYTES: u64 = 64 << 20;
33
35
  const MEMBER_MAX: usize = 4096;
34
36
  /// A `*.safetensors.index.json` is a map from tensor name to shard file. It is JSON that
35
37
  /// git itself keeps small, and the Go resolver bounded it at the same 32 MiB.
@@ -250,6 +252,17 @@ fn list_observed(
250
252
  credentials: &dyn CredentialProvider,
251
253
  deadline: Deadline,
252
254
  ledger: &Ledger,
255
+ ) -> Result<Vec<SourceMember>> {
256
+ list_selected_observed(uri, endpoints, credentials, deadline, ledger, None)
257
+ }
258
+
259
+ fn list_selected_observed(
260
+ uri: &SourceUri,
261
+ endpoints: &Endpoints,
262
+ credentials: &dyn CredentialProvider,
263
+ deadline: Deadline,
264
+ ledger: &Ledger,
265
+ files: Option<&[String]>,
253
266
  ) -> Result<Vec<SourceMember>> {
254
267
  let policy = endpoints.policy(uri);
255
268
  let base = endpoints.base(uri);
@@ -287,6 +300,9 @@ fn list_observed(
287
300
  let Some(path) = json_str(row, "path") else {
288
301
  return refuse(Code::MISSING_FIELD, "a tree row names no path");
289
302
  };
303
+ if files.is_some_and(|files| !files.iter().any(|file| file == path)) {
304
+ continue;
305
+ }
290
306
  if !member_path(path) {
291
307
  // Every HuggingFace repository carries `.gitattributes`, and a
292
308
  // leading dot is outside the member grammar. Refusing the whole
@@ -345,7 +361,10 @@ fn list_observed(
345
361
  member: path.to_string(),
346
362
  length,
347
363
  sha256,
348
- url: format!("{base}/{org}/{repo}/resolve/{revision}/{path}"),
364
+ url: format!(
365
+ "{base}/{org}/{repo}/resolve/{revision}/{}",
366
+ path.replace(' ', "%20")
367
+ ),
349
368
  primary: false,
350
369
  });
351
370
  }
@@ -1342,6 +1361,145 @@ pub fn resolve(
1342
1361
  })
1343
1362
  }
1344
1363
 
1364
+ /// Resolve only explicitly named ordinary files at one immutable HF revision.
1365
+ /// No suffix has special meaning here: selecting Python bytes never executes them,
1366
+ /// and selecting an index does not expand it into tensor shards. Tensor carriers
1367
+ /// continue to use `resolve` and reviewed profile/carrier selection.
1368
+ pub fn resolve_files(
1369
+ uri: &SourceUri,
1370
+ files: &[String],
1371
+ endpoints: &Endpoints,
1372
+ credentials: &dyn CredentialProvider,
1373
+ deadline: Deadline,
1374
+ ) -> Result<Resolution> {
1375
+ let SourceUri::HuggingFace {
1376
+ org,
1377
+ repo,
1378
+ revision,
1379
+ } = uri
1380
+ else {
1381
+ return refuse(
1382
+ Code::SOURCE_NOT_ALLOWED,
1383
+ "ordinary file selection requires HuggingFace",
1384
+ );
1385
+ };
1386
+ if files.is_empty() || files.len() > MEMBER_MAX {
1387
+ return refuse(
1388
+ Code::COUNT_CAP,
1389
+ "ordinary file selection count is outside the bound",
1390
+ );
1391
+ }
1392
+ let mut names = files.to_vec();
1393
+ names.sort();
1394
+ if names
1395
+ .iter()
1396
+ .any(|name| !member_path(name) || crate::manifest::check_path(name).is_err())
1397
+ {
1398
+ return refuse(
1399
+ Code::PATH_ILLEGAL,
1400
+ "ordinary file selection requires exact member paths",
1401
+ );
1402
+ }
1403
+ if names.windows(2).any(|pair| pair[0] == pair[1]) {
1404
+ return refuse(
1405
+ Code::DUPLICATE_KEY,
1406
+ "ordinary file selection names one member twice",
1407
+ );
1408
+ }
1409
+ let ledger = Ledger::new();
1410
+ let policy = endpoints.policy(uri);
1411
+ let listed =
1412
+ list_selected_observed(uri, endpoints, credentials, deadline, &ledger, Some(&names))?;
1413
+ // Establish the whole roster before fetching any body. A missing later member
1414
+ // must not turn a partial requested tree into a successful smaller selection.
1415
+ let selected = names
1416
+ .iter()
1417
+ .map(|name| {
1418
+ listed
1419
+ .iter()
1420
+ .find(|member| &member.member == name)
1421
+ .ok_or_else(|| Refusal {
1422
+ code: Code::MISSING_FIELD,
1423
+ detail: format!("the source does not contain requested file {name:?}"),
1424
+ })
1425
+ })
1426
+ .collect::<Result<Vec<_>>>()?;
1427
+ let listed_bytes = selected.iter().try_fold(0u64, |total, member| {
1428
+ total.checked_add(member.length.unwrap_or(0))
1429
+ });
1430
+ if listed_bytes.is_none_or(|bytes| bytes > FILE_SELECTION_MAX_BYTES) {
1431
+ return refuse(
1432
+ Code::SIZE_CAP,
1433
+ "ordinary file selection exceeds its byte bound",
1434
+ );
1435
+ }
1436
+ let mut remaining = FILE_SELECTION_MAX_BYTES;
1437
+ let mut resolved = Vec::with_capacity(selected.len());
1438
+ for member in selected {
1439
+ if member
1440
+ .length
1441
+ .map_or(remaining == 0, |length| length > remaining)
1442
+ {
1443
+ return refuse(
1444
+ Code::SIZE_CAP,
1445
+ "ordinary file selection exceeds its byte bound",
1446
+ );
1447
+ }
1448
+ let (object, provenance) = if let Some(sha256) = &member.sha256 {
1449
+ let length = match member.length {
1450
+ Some(length) => length,
1451
+ None => probe_length(&member.url, &policy, credentials, deadline, &ledger)?,
1452
+ };
1453
+ (
1454
+ ObjectRef {
1455
+ sha256: sha256.clone(),
1456
+ length,
1457
+ },
1458
+ Provenance::Declared,
1459
+ )
1460
+ } else {
1461
+ let (body, _) = api_get(
1462
+ &member.url,
1463
+ &policy,
1464
+ credentials,
1465
+ remaining,
1466
+ deadline,
1467
+ &ledger,
1468
+ )?;
1469
+ if member
1470
+ .length
1471
+ .is_some_and(|length| length != body.len() as u64)
1472
+ {
1473
+ return refuse(
1474
+ Code::LENGTH_MISMATCH,
1475
+ "ordinary file body disagrees with listed length",
1476
+ );
1477
+ }
1478
+ (ObjectRef::of(&body), Provenance::Computed)
1479
+ };
1480
+ remaining = remaining
1481
+ .checked_sub(object.length)
1482
+ .ok_or_else(|| Refusal {
1483
+ code: Code::SIZE_CAP,
1484
+ detail: "ordinary file selection exceeds its byte bound".into(),
1485
+ })?;
1486
+ resolved.push(ResolvedMember {
1487
+ member: member.member.clone(),
1488
+ object,
1489
+ url: member.url.clone(),
1490
+ provenance,
1491
+ carrier: false,
1492
+ requires: Vec::new(),
1493
+ });
1494
+ }
1495
+ let canonical = format!("hf://{org}/{repo}@{revision}");
1496
+ Ok(Resolution {
1497
+ selection_sha256: selection_digest(&canonical, &resolved),
1498
+ canonical,
1499
+ members: resolved,
1500
+ })
1501
+ }
1502
+
1345
1503
  #[cfg(test)]
1346
1504
  mod tests {
1347
1505
  use super::*;
@@ -2231,4 +2389,320 @@ mod tests {
2231
2389
  handle.join().unwrap();
2232
2390
  let _ = std::fs::remove_dir_all(root);
2233
2391
  }
2392
+
2393
+ #[test]
2394
+ fn explicit_files_resolve_only_named_bytes_and_retain_across_restart() {
2395
+ let config = b"{\"model_type\":\"test\"}".to_vec();
2396
+ let tokenizer = b"these are tokenizer bytes".to_vec();
2397
+ let expected = vec![ObjectRef::of(&config), ObjectRef::of(&tokenizer)];
2398
+ let listing = format!(
2399
+ r#"[
2400
+ {{"type":"file","path":"model.safetensors","size":100000000000,"lfs":{{"oid":"{}","size":100000000000}}}},
2401
+ {{"type":"file","path":"config.json","size":{}}},
2402
+ {{"type":"file","path":"tokenizer files/tokenizer.json","size":{}}},
2403
+ {{"type":"file","path":"unused.py","size":99}}
2404
+ ]"#,
2405
+ "*".repeat(64),
2406
+ config.len(),
2407
+ tokenizer.len()
2408
+ );
2409
+ // Two order-independent resolutions each read list + two small bodies;
2410
+ // materialization reads the bodies once, and restart/warm admission reads none.
2411
+ let (base, seen, handle) = origin(8, move |request| {
2412
+ if request.path.contains("/tree/") {
2413
+ ok_body(listing.as_bytes(), "application/json")
2414
+ } else if request.path.ends_with("/config.json") {
2415
+ ok_body(&config, "application/json")
2416
+ } else if request.path.ends_with("/tokenizer.json") {
2417
+ ok_body(&tokenizer, "application/json")
2418
+ } else {
2419
+ panic!("unselected file requested: {}", request.path);
2420
+ }
2421
+ });
2422
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2423
+ let endpoints = Endpoints {
2424
+ huggingface: base,
2425
+ allow_local: true,
2426
+ ..Endpoints::default()
2427
+ };
2428
+ let mut names = vec![
2429
+ "config.json".into(),
2430
+ "tokenizer files/tokenizer.json".into(),
2431
+ ];
2432
+ let selected =
2433
+ resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none()).unwrap();
2434
+ names.reverse();
2435
+ assert_eq!(
2436
+ selected,
2437
+ resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none()).unwrap()
2438
+ );
2439
+ assert_eq!(
2440
+ selected
2441
+ .members
2442
+ .iter()
2443
+ .map(|m| m.object.clone())
2444
+ .collect::<Vec<_>>(),
2445
+ expected
2446
+ );
2447
+ assert!(selected
2448
+ .members
2449
+ .iter()
2450
+ .all(|m| m.provenance == Provenance::Computed && !m.carrier && m.requires.is_empty()));
2451
+ let root = temporary("selected-files");
2452
+ let store = Store::init(&root).unwrap();
2453
+ let producer = ObjectRef::of(b"files-producer").id();
2454
+ let recipient = ObjectRef::of(b"files-recipient").id();
2455
+ let (cold, transfers) = materialize_selected(
2456
+ &store,
2457
+ &producer,
2458
+ &selected,
2459
+ &endpoints.policy(&uri),
2460
+ &Anonymous,
2461
+ Deadline::none(),
2462
+ &mut |_, _| {},
2463
+ )
2464
+ .unwrap();
2465
+ assert_eq!(
2466
+ transfers.iter().map(|row| row.transferred).sum::<u64>(),
2467
+ selected.member_bytes()
2468
+ );
2469
+ crate::source_artifact::retain(&store, &producer, &recipient).unwrap();
2470
+ drop(store);
2471
+ let store = Store::open(&root).unwrap();
2472
+ let (warm, transfers) = materialize_selected(
2473
+ &store,
2474
+ &producer,
2475
+ &selected,
2476
+ &endpoints.policy(&uri),
2477
+ &Anonymous,
2478
+ Deadline::none(),
2479
+ &mut |_, _| {},
2480
+ )
2481
+ .unwrap();
2482
+ assert_eq!(warm, cold);
2483
+ assert!(transfers.iter().all(|row| row.held && row.transferred == 0));
2484
+ crate::source_artifact::release(&store, &producer).unwrap();
2485
+ crate::gc::collect(&root, false).unwrap();
2486
+ let retained = crate::source_artifact::read(&store, &recipient)
2487
+ .unwrap()
2488
+ .unwrap();
2489
+ assert!(retained.complete && !retained.released);
2490
+ for object in &expected {
2491
+ assert!(store.record_valid(&object.sha256).is_ok());
2492
+ }
2493
+ crate::source_artifact::release(&store, &recipient).unwrap();
2494
+ crate::gc::collect(&root, false).unwrap();
2495
+ for object in expected {
2496
+ assert!(!store.contains(&object.sha256));
2497
+ }
2498
+ handle.join().unwrap();
2499
+ assert_eq!(seen.lock().unwrap().len(), 8);
2500
+ assert!(seen
2501
+ .lock()
2502
+ .unwrap()
2503
+ .iter()
2504
+ .any(|(path, _)| path.contains("tokenizer%20files/")));
2505
+ std::fs::remove_dir_all(root).unwrap();
2506
+ }
2507
+
2508
+ #[test]
2509
+ fn explicit_files_refuse_missing_and_over_budget_before_fetching_bodies() {
2510
+ for (names, listing, code) in [
2511
+ (
2512
+ vec!["absent.json".into(), "config.json".into()],
2513
+ r#"[{"type":"file","path":"config.json","size":2}]"#.to_string(),
2514
+ Code::MISSING_FIELD,
2515
+ ),
2516
+ (
2517
+ vec!["config.json".into()],
2518
+ format!(
2519
+ r#"[{{"type":"file","path":"config.json","size":{}}}]"#,
2520
+ FILE_SELECTION_MAX_BYTES + 1
2521
+ ),
2522
+ Code::SIZE_CAP,
2523
+ ),
2524
+ ] {
2525
+ let (base, _, handle) =
2526
+ origin(1, move |_| ok_body(listing.as_bytes(), "application/json"));
2527
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2528
+ let endpoints = Endpoints {
2529
+ huggingface: base,
2530
+ allow_local: true,
2531
+ ..Endpoints::default()
2532
+ };
2533
+ assert_eq!(
2534
+ resolve_files(&uri, &names, &endpoints, &Anonymous, Deadline::none())
2535
+ .unwrap_err()
2536
+ .code,
2537
+ code
2538
+ );
2539
+ handle.join().unwrap();
2540
+ }
2541
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2542
+ for names in [
2543
+ vec!["../config.json".into()],
2544
+ vec!["*.json".into()],
2545
+ vec!["/config.json".into()],
2546
+ ] {
2547
+ assert_eq!(
2548
+ resolve_files(
2549
+ &uri,
2550
+ &names,
2551
+ &Endpoints::default(),
2552
+ &Anonymous,
2553
+ Deadline::none()
2554
+ )
2555
+ .unwrap_err()
2556
+ .code,
2557
+ Code::PATH_ILLEGAL
2558
+ );
2559
+ }
2560
+ assert_eq!(
2561
+ resolve_files(
2562
+ &uri,
2563
+ &["config.json".into(), "config.json".into()],
2564
+ &Endpoints::default(),
2565
+ &Anonymous,
2566
+ Deadline::none()
2567
+ )
2568
+ .unwrap_err()
2569
+ .code,
2570
+ Code::DUPLICATE_KEY
2571
+ );
2572
+ }
2573
+
2574
+ #[test]
2575
+ fn explicit_files_refuse_a_changed_listing_length() {
2576
+ let (base, _, handle) = origin(2, move |request| {
2577
+ if request.path.contains("/tree/") {
2578
+ ok_body(
2579
+ br#"[{"type":"file","path":"config.json","size":2}]"#,
2580
+ "application/json",
2581
+ )
2582
+ } else {
2583
+ ok_body(b"changed", "application/json")
2584
+ }
2585
+ });
2586
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2587
+ let endpoints = Endpoints {
2588
+ huggingface: base,
2589
+ allow_local: true,
2590
+ ..Endpoints::default()
2591
+ };
2592
+ assert_eq!(
2593
+ resolve_files(
2594
+ &uri,
2595
+ &["config.json".into()],
2596
+ &endpoints,
2597
+ &Anonymous,
2598
+ Deadline::none()
2599
+ )
2600
+ .unwrap_err()
2601
+ .code,
2602
+ Code::LENGTH_MISMATCH
2603
+ );
2604
+ handle.join().unwrap();
2605
+ }
2606
+
2607
+ #[test]
2608
+ fn explicit_files_changed_after_resolution_never_finish_a_native_root() {
2609
+ let replies = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2610
+ let (base, _, handle) = origin(3, move |request| {
2611
+ if request.path.contains("/tree/") {
2612
+ ok_body(
2613
+ br#"[{"type":"file","path":"config.json","size":2}]"#,
2614
+ "application/json",
2615
+ )
2616
+ } else {
2617
+ let attempt = replies.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2618
+ ok_body(if attempt == 0 { b"AA" } else { b"BB" }, "application/json")
2619
+ }
2620
+ });
2621
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2622
+ let endpoints = Endpoints {
2623
+ huggingface: base,
2624
+ allow_local: true,
2625
+ ..Endpoints::default()
2626
+ };
2627
+ let selected = resolve_files(
2628
+ &uri,
2629
+ &["config.json".into()],
2630
+ &endpoints,
2631
+ &Anonymous,
2632
+ Deadline::none(),
2633
+ )
2634
+ .unwrap();
2635
+ let root = temporary("changed-files");
2636
+ let store = Store::init(&root).unwrap();
2637
+ let producer = ObjectRef::of(b"changed-files-producer").id();
2638
+ let error = materialize_selected(
2639
+ &store,
2640
+ &producer,
2641
+ &selected,
2642
+ &endpoints.policy(&uri),
2643
+ &Anonymous,
2644
+ Deadline::none(),
2645
+ &mut |_, _| {},
2646
+ )
2647
+ .unwrap_err();
2648
+ assert_eq!(error.code, Code::OBJECT_ID_MISMATCH);
2649
+ assert!(!store.contains(&selected.members[0].object.sha256));
2650
+ assert!(
2651
+ !crate::source_artifact::read(&store, &producer)
2652
+ .unwrap()
2653
+ .unwrap()
2654
+ .complete
2655
+ );
2656
+ handle.join().unwrap();
2657
+ std::fs::remove_dir_all(root).unwrap();
2658
+ }
2659
+
2660
+ #[test]
2661
+ fn explicit_files_can_retain_an_empty_file() {
2662
+ let (base, _, handle) = origin(2, move |request| {
2663
+ if request.path.contains("/tree/") {
2664
+ ok_body(
2665
+ br#"[{"type":"file","path":"empty.py","size":0}]"#,
2666
+ "application/json",
2667
+ )
2668
+ } else {
2669
+ ok_body(b"", "text/plain")
2670
+ }
2671
+ });
2672
+ let uri = SourceUri::parse(&format!("hf://org/repo@{}", "ab".repeat(20))).unwrap();
2673
+ let endpoints = Endpoints {
2674
+ huggingface: base,
2675
+ allow_local: true,
2676
+ ..Endpoints::default()
2677
+ };
2678
+ let selected = resolve_files(
2679
+ &uri,
2680
+ &["empty.py".into()],
2681
+ &endpoints,
2682
+ &Anonymous,
2683
+ Deadline::none(),
2684
+ )
2685
+ .unwrap();
2686
+ assert_eq!(selected.members[0].object, ObjectRef::of(b""));
2687
+ let root = temporary("empty-files");
2688
+ let store = Store::init(&root).unwrap();
2689
+ let producer = ObjectRef::of(b"empty-files-producer").id();
2690
+ let (retained, transfers) = materialize_selected(
2691
+ &store,
2692
+ &producer,
2693
+ &selected,
2694
+ &endpoints.policy(&uri),
2695
+ &Anonymous,
2696
+ Deadline::none(),
2697
+ &mut |_, _| {},
2698
+ )
2699
+ .unwrap();
2700
+ assert!(retained.complete && !retained.released);
2701
+ assert_eq!(transfers[0].transferred, 0);
2702
+ assert!(store
2703
+ .record_valid(&selected.members[0].object.sha256)
2704
+ .is_ok());
2705
+ handle.join().unwrap();
2706
+ std::fs::remove_dir_all(root).unwrap();
2707
+ }
2234
2708
  }