whats2000 commited on
Commit
450c8b2
·
1 Parent(s): d25b7a0

feat(eda): migrate to Dask distributed with adaptive scaling and memory limits

Browse files
Files changed (3) hide show
  1. pyproject.toml +1 -0
  2. scripts/distributed_eda.py +99 -122
  3. uv.lock +213 -0
pyproject.toml CHANGED
@@ -19,4 +19,5 @@ dependencies = [
19
  "joblib>=1.3.0",
20
  "pyarrow>=23.0.0",
21
  "fastparquet>=2025.12.0",
 
22
  ]
 
19
  "joblib>=1.3.0",
20
  "pyarrow>=23.0.0",
21
  "fastparquet>=2025.12.0",
22
+ "dask[distributed]>=2026.1.2",
23
  ]
scripts/distributed_eda.py CHANGED
@@ -22,6 +22,7 @@ import yaml
22
  from concurrent.futures.process import BrokenProcessPool
23
  from scipy import sparse
24
  from tqdm import tqdm
 
25
 
26
 
27
  @dataclass
@@ -518,9 +519,10 @@ def process_failed_datasets(
518
 
519
  # Reduce parallelism on retries to avoid resource contention
520
  retry_workers = max(1, config["resources"]["max_workers"] // (2 ** retry_attempt))
 
521
 
522
  print(f"\nRetry attempt {retry_attempt + 1}/{max_retries}: {len(remaining)} datasets")
523
- print(f"Using {retry_workers} workers (reduced to avoid resource issues)")
524
 
525
  wait_time = 2 ** retry_attempt # 1s, 2s, 4s exponential backoff
526
  if wait_time > 0:
@@ -529,20 +531,31 @@ def process_failed_datasets(
529
 
530
  next_remaining = []
531
 
532
- with tqdm(total=len(remaining), desc=f"Retry {retry_attempt + 1}", unit="dataset") as pbar:
533
- executor = concurrent.futures.ProcessPoolExecutor(max_workers=retry_workers)
534
- futures = {}
535
-
536
- try:
 
 
 
 
 
 
 
 
 
 
537
  for path, size_cat, strategy in remaining:
538
  try:
539
- fut = executor.submit(
540
  run_with_strategy_single_attempt,
541
  path,
542
  size_cat,
543
  strategy,
544
  config,
545
  per_dataset_dir,
 
546
  )
547
  futures[fut] = (path, size_cat, strategy)
548
  except Exception as exc:
@@ -550,7 +563,7 @@ def process_failed_datasets(
550
  tqdm.write(f"[retry failed to submit] {path.name}: {exc}")
551
  pbar.update(1)
552
 
553
- for fut in concurrent.futures.as_completed(futures):
554
  path, size_cat, strategy = futures[fut]
555
  try:
556
  row = fut.result(timeout=3600)
@@ -562,16 +575,14 @@ def process_failed_datasets(
562
  row["retry_attempt"] = retry_attempt + 1
563
  successes.append(row)
564
  tqdm.write(f"[retry ok] {path.name} (attempt {retry_attempt + 1})")
565
- except concurrent.futures.TimeoutError:
566
- next_remaining.append((path, size_cat, strategy))
567
- tqdm.write(f"[retry timeout] {path.name}")
568
  except Exception as exc:
569
  next_remaining.append((path, size_cat, strategy))
570
  tqdm.write(f"[retry error] {path.name}: {exc}")
571
  finally:
572
  pbar.update(1)
573
- finally:
574
- executor.shutdown(wait=True)
 
575
 
576
  remaining = next_remaining
577
 
@@ -596,9 +607,9 @@ def process_stage(
596
  per_dataset_dir: Path,
597
  max_workers: int,
598
  ) -> tuple[list[dict], list[tuple[Path, str, dict]]]:
599
- """Process datasets in a stage sequentially with batched executor recycling.
600
 
601
- Adaptively reduces workers when throughput drops, indicating heavier datasets.
602
  """
603
  if not tasks:
604
  return [], []
@@ -606,124 +617,90 @@ def process_stage(
606
  successes = []
607
  failures = []
608
 
609
- # Adaptive worker tracking
610
- current_workers = max_workers
611
- baseline_throughput = None # Initial stable throughput
612
- last_check_idx = 0
613
- last_check_time = time.time()
614
-
615
- # Batch size: process in smaller batches to allow executor recreation
616
- # This prevents memory buildup in the process pool
617
- # Smaller batches with fewer workers process faster and recycle more frequently
618
- batch_size = max(30, min(100, len(tasks) // 4)) # 30-100 datasets per batch
619
-
620
- # Adaptive tuning parameters
621
- check_interval = 50 # Check throughput every N datasets
622
- slowdown_threshold = 0.5 # Reduce workers if throughput drops to 50% of baseline
623
- min_workers = max(1, max_workers // 4) # Don't go below 25% of initial
624
 
625
  print(f"\n{'='*80}")
626
  print(f"Stage: {stage_name.upper()} ({len(tasks)} datasets)")
627
- print(f"Workers: {current_workers} (adaptive: {min_workers}-{max_workers})")
628
- print(f"Batch size: {batch_size} (executor recycled between batches)")
629
  print(f"{'='*80}\n")
630
 
631
- overall_pbar = tqdm(total=len(tasks), desc=f"{stage_name}", unit="dataset")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
 
633
  try:
634
- # Process in batches
635
- for batch_start in range(0, len(tasks), batch_size):
636
- batch_end = min(batch_start + batch_size, len(tasks))
637
- batch_tasks = tasks[batch_start:batch_end]
638
- batch_num = (batch_start // batch_size) + 1
639
- total_batches = (len(tasks) + batch_size - 1) // batch_size
640
-
641
- # Check if we should reduce workers based on throughput
642
- processed_count = len(successes) + len(failures)
643
- if processed_count >= last_check_idx + check_interval and processed_count > check_interval:
644
- current_time = time.time()
645
- delta_count = processed_count - last_check_idx
646
- delta_time = current_time - last_check_time
647
- recent_throughput = delta_count / delta_time if delta_time > 0 else 0
648
-
649
- # Establish baseline from first measurement window
650
- if baseline_throughput is None and processed_count >= check_interval * 2:
651
- baseline_throughput = recent_throughput
652
- tqdm.write(f"[{stage_name}] Baseline throughput: {baseline_throughput:.2f} datasets/sec")
653
-
654
- # Check for slowdown
655
- if baseline_throughput and recent_throughput < baseline_throughput * slowdown_threshold:
656
- if current_workers > min_workers:
657
- old_workers = current_workers
658
- current_workers = max(min_workers, current_workers // 2)
659
- tqdm.write(f"[{stage_name}] ⚠️ Throughput dropped to {recent_throughput:.2f} datasets/sec (baseline: {baseline_throughput:.2f})")
660
- tqdm.write(f"[{stage_name}] 🔧 Reducing workers: {old_workers} → {current_workers}")
661
- baseline_throughput = None # Reset baseline after adjustment
662
-
663
- last_check_idx = processed_count
664
- last_check_time = current_time
665
-
666
- if len(tasks) > batch_size:
667
- tqdm.write(f"\n[{stage_name}] Batch {batch_num}/{total_batches} ({len(batch_tasks)} datasets, {current_workers} workers)")
668
-
669
- # Create fresh executor for each batch with current worker count
670
- executor = concurrent.futures.ProcessPoolExecutor(max_workers=current_workers)
671
  futures = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
 
673
- try:
674
- # Submit batch tasks
675
- for path, size_cat, strategy in batch_tasks:
676
- try:
677
- fut = executor.submit(
678
- run_with_strategy_single_attempt,
679
- path,
680
- size_cat,
681
- strategy,
682
- config,
683
- per_dataset_dir,
684
- )
685
- futures[fut] = (path, size_cat, strategy)
686
- except Exception as exc:
687
  failures.append((path, size_cat, strategy))
688
- tqdm.write(f"[{stage_name}] submit failed: {path.name}: {exc}")
689
- overall_pbar.update(1)
690
-
691
- # Collect batch results
692
- for fut in concurrent.futures.as_completed(futures):
693
- path, size_cat, strategy = futures[fut]
694
- try:
695
- row = fut.result(timeout=7200) # 2 hour timeout
696
-
697
- if row.get("status") in ("failed", "failed_after_retries"):
698
- failures.append((path, size_cat, strategy))
699
- error_msg = row.get('error', 'Unknown')[:80]
700
- tqdm.write(f"[{stage_name}] failed: {path.name}: {error_msg}")
701
- else:
702
- row["stage"] = stage_name
703
- row["batch"] = batch_num
704
- successes.append(row)
705
- if config["behavior"]["log_each_dataset"]:
706
- elapsed = row.get("elapsed_sec", "?")
707
- tqdm.write(f"[{stage_name}] ok: {path.name} ({elapsed}s)")
708
- except concurrent.futures.TimeoutError:
709
- failures.append((path, size_cat, strategy))
710
- tqdm.write(f"[{stage_name}] timeout: {path.name}")
711
- except (BrokenProcessPool, Exception) as exc:
712
- failures.append((path, size_cat, strategy))
713
- tqdm.write(f"[{stage_name}] error: {path.name}: {exc}")
714
- finally:
715
- overall_pbar.update(1)
716
- finally:
717
- # Shutdown executor after each batch
718
- executor.shutdown(wait=True)
719
- # Force cleanup between batches
720
- gc.collect()
721
- time.sleep(1) # Brief pause for cleanup
722
  finally:
723
- overall_pbar.close()
724
-
725
- # Final cleanup after stage
726
- gc.collect()
727
 
728
  print(f"\n{stage_name.upper()} complete: {len(successes)} succeeded, {len(failures)} failed")
729
 
 
22
  from concurrent.futures.process import BrokenProcessPool
23
  from scipy import sparse
24
  from tqdm import tqdm
25
+ from dask.distributed import Client, LocalCluster, as_completed
26
 
27
 
28
  @dataclass
 
519
 
520
  # Reduce parallelism on retries to avoid resource contention
521
  retry_workers = max(1, config["resources"]["max_workers"] // (2 ** retry_attempt))
522
+ memory_per_worker_gib = config["resources"]["max_memory_gib"] / retry_workers
523
 
524
  print(f"\nRetry attempt {retry_attempt + 1}/{max_retries}: {len(remaining)} datasets")
525
+ print(f"Using {retry_workers} workers, {memory_per_worker_gib:.1f} GiB per worker")
526
 
527
  wait_time = 2 ** retry_attempt # 1s, 2s, 4s exponential backoff
528
  if wait_time > 0:
 
531
 
532
  next_remaining = []
533
 
534
+ # Use Dask for retries with conservative settings
535
+ cluster = LocalCluster(
536
+ n_workers=retry_workers,
537
+ threads_per_worker=1,
538
+ processes=True,
539
+ memory_limit=f"{memory_per_worker_gib}GiB",
540
+ silence_logs=True,
541
+ dashboard_address=None,
542
+ )
543
+ client = Client(cluster)
544
+
545
+ try:
546
+ with tqdm(total=len(remaining), desc=f"Retry {retry_attempt + 1}", unit="dataset") as pbar:
547
+ futures = {}
548
+
549
  for path, size_cat, strategy in remaining:
550
  try:
551
+ fut = client.submit(
552
  run_with_strategy_single_attempt,
553
  path,
554
  size_cat,
555
  strategy,
556
  config,
557
  per_dataset_dir,
558
+ pure=False,
559
  )
560
  futures[fut] = (path, size_cat, strategy)
561
  except Exception as exc:
 
563
  tqdm.write(f"[retry failed to submit] {path.name}: {exc}")
564
  pbar.update(1)
565
 
566
+ for fut in as_completed(futures):
567
  path, size_cat, strategy = futures[fut]
568
  try:
569
  row = fut.result(timeout=3600)
 
575
  row["retry_attempt"] = retry_attempt + 1
576
  successes.append(row)
577
  tqdm.write(f"[retry ok] {path.name} (attempt {retry_attempt + 1})")
 
 
 
578
  except Exception as exc:
579
  next_remaining.append((path, size_cat, strategy))
580
  tqdm.write(f"[retry error] {path.name}: {exc}")
581
  finally:
582
  pbar.update(1)
583
+ finally:
584
+ client.close()
585
+ cluster.close()
586
 
587
  remaining = next_remaining
588
 
 
607
  per_dataset_dir: Path,
608
  max_workers: int,
609
  ) -> tuple[list[dict], list[tuple[Path, str, dict]]]:
610
+ """Process datasets using Dask LocalCluster with adaptive scaling.
611
 
612
+ Dask automatically manages memory per worker and scales workers based on load.
613
  """
614
  if not tasks:
615
  return [], []
 
617
  successes = []
618
  failures = []
619
 
620
+ # Calculate memory per worker from config
621
+ max_memory_gib = config["resources"]["max_memory_gib"]
622
+ memory_per_worker_gib = max_memory_gib / max_workers
623
+ min_workers = max(1, max_workers // 4)
 
 
 
 
 
 
 
 
 
 
 
624
 
625
  print(f"\n{'='*80}")
626
  print(f"Stage: {stage_name.upper()} ({len(tasks)} datasets)")
627
+ print(f"Workers: {min_workers}-{max_workers} (adaptive via Dask)")
628
+ print(f"Memory per worker: {memory_per_worker_gib:.1f} GiB")
629
  print(f"{'='*80}\n")
630
 
631
+ # Create Dask LocalCluster with memory limits
632
+ cluster = LocalCluster(
633
+ n_workers=min_workers, # Start with minimum
634
+ threads_per_worker=1, # Single-threaded for memory isolation
635
+ processes=True,
636
+ memory_limit=f"{memory_per_worker_gib}GiB",
637
+ silence_logs=True,
638
+ dashboard_address=None, # Disable dashboard for HPC
639
+ )
640
+
641
+ # Enable adaptive scaling
642
+ cluster.adapt(
643
+ minimum=min_workers,
644
+ maximum=max_workers,
645
+ target_duration='30s', # Target 30s task duration
646
+ wait_count=3, # Wait for 3 tasks before scaling up
647
+ )
648
+
649
+ client = Client(cluster)
650
 
651
  try:
652
+ tqdm.write(f"[{stage_name}] Dask cluster started: {client.dashboard_link if hasattr(client, 'dashboard_link') else 'N/A'}")
653
+
654
+ overall_pbar = tqdm(total=len(tasks), desc=f"{stage_name}", unit="dataset")
655
+
656
+ try:
657
+ # Submit all tasks to Dask
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  futures = {}
659
+ for path, size_cat, strategy in tasks:
660
+ try:
661
+ fut = client.submit(
662
+ run_with_strategy_single_attempt,
663
+ path,
664
+ size_cat,
665
+ strategy,
666
+ config,
667
+ per_dataset_dir,
668
+ pure=False, # Don't cache results (each task is unique)
669
+ )
670
+ futures[fut] = (path, size_cat, strategy)
671
+ except Exception as exc:
672
+ failures.append((path, size_cat, strategy))
673
+ tqdm.write(f"[{stage_name}] submit failed: {path.name}: {exc}")
674
+ overall_pbar.update(1)
675
 
676
+ # Collect results as they complete
677
+ for fut in as_completed(futures):
678
+ path, size_cat, strategy = futures[fut]
679
+ try:
680
+ row = fut.result(timeout=7200) # 2 hour timeout
681
+
682
+ if row.get("status") in ("failed", "failed_after_retries"):
 
 
 
 
 
 
 
683
  failures.append((path, size_cat, strategy))
684
+ error_msg = row.get('error', 'Unknown')[:80]
685
+ tqdm.write(f"[{stage_name}] failed: {path.name}: {error_msg}")
686
+ else:
687
+ row["stage"] = stage_name
688
+ successes.append(row)
689
+ if config["behavior"]["log_each_dataset"]:
690
+ elapsed = row.get("elapsed_sec", "?")
691
+ tqdm.write(f"[{stage_name}] ok: {path.name} ({elapsed}s)")
692
+ except Exception as exc:
693
+ failures.append((path, size_cat, strategy))
694
+ tqdm.write(f"[{stage_name}] error: {path.name}: {exc}")
695
+ finally:
696
+ overall_pbar.update(1)
697
+ finally:
698
+ overall_pbar.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  finally:
700
+ # Cleanup Dask cluster
701
+ client.close()
702
+ cluster.close()
703
+ gc.collect()
704
 
705
  print(f"\n{stage_name.upper()} complete: {len(successes)} succeeded, {len(failures)} failed")
706
 
uv.lock CHANGED
@@ -224,6 +224,7 @@ source = { virtual = "." }
224
  dependencies = [
225
  { name = "anndata", version = "0.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
226
  { name = "anndata", version = "0.12.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
 
227
  { name = "fastparquet" },
228
  { name = "ipykernel" },
229
  { name = "joblib" },
@@ -245,6 +246,7 @@ dependencies = [
245
  [package.metadata]
246
  requires-dist = [
247
  { name = "anndata", specifier = ">=0.10.0" },
 
248
  { name = "fastparquet", specifier = ">=2025.12.0" },
249
  { name = "ipykernel", specifier = ">=6.25.0" },
250
  { name = "joblib", specifier = ">=1.3.0" },
@@ -454,6 +456,27 @@ wheels = [
454
  { url = "https://files.pythonhosted.org/packages/b7/9f/d73dfb85d7a5b1a56a99adc50f2074029468168c970ff5daeade4ad819e4/choreographer-1.2.1-py3-none-any.whl", hash = "sha256:9af5385effa3c204dbc337abf7ac74fd8908ced326a15645dc31dde75718c77e", size = 49338, upload-time = "2025-11-09T23:04:43.154Z" },
455
  ]
456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  [[package]]
458
  name = "colorama"
459
  version = "0.4.6"
@@ -748,6 +771,30 @@ wheels = [
748
  { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
749
  ]
750
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  [[package]]
752
  name = "debugpy"
753
  version = "1.8.20"
@@ -795,6 +842,32 @@ wheels = [
795
  { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
796
  ]
797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
  [[package]]
799
  name = "donfig"
800
  version = "0.8.1.post1"
@@ -1128,6 +1201,18 @@ wheels = [
1128
  { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
1129
  ]
1130
 
 
 
 
 
 
 
 
 
 
 
 
 
1131
  [[package]]
1132
  name = "iniconfig"
1133
  version = "2.3.0"
@@ -1717,6 +1802,15 @@ wheels = [
1717
  { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" },
1718
  ]
1719
 
 
 
 
 
 
 
 
 
 
1720
  [[package]]
1721
  name = "logistro"
1722
  version = "2.0.1"
@@ -1910,6 +2004,67 @@ wheels = [
1910
  { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
1911
  ]
1912
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1913
  [[package]]
1914
  name = "narwhals"
1915
  version = "2.16.0"
@@ -2441,6 +2596,19 @@ wheels = [
2441
  { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" },
2442
  ]
2443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2444
  [[package]]
2445
  name = "patsy"
2446
  version = "1.0.2"
@@ -3594,6 +3762,15 @@ wheels = [
3594
  { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
3595
  ]
3596
 
 
 
 
 
 
 
 
 
 
3597
  [[package]]
3598
  name = "soupsieve"
3599
  version = "2.8.3"
@@ -3664,6 +3841,15 @@ wheels = [
3664
  { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" },
3665
  ]
3666
 
 
 
 
 
 
 
 
 
 
3667
  [[package]]
3668
  name = "terminado"
3669
  version = "0.18.1"
@@ -3753,6 +3939,15 @@ wheels = [
3753
  { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
3754
  ]
3755
 
 
 
 
 
 
 
 
 
 
3756
  [[package]]
3757
  name = "tornado"
3758
  version = "6.5.4"
@@ -3910,3 +4105,21 @@ sdist = { url = "https://files.pythonhosted.org/packages/fc/76/7fa87f57c112c7b9c
3910
  wheels = [
3911
  { url = "https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl", hash = "sha256:29cd905afb6235b94c09decda4258c888fcb79bb6c862ef7c0b8fe009b5c8563", size = 284067, upload-time = "2025-11-21T14:05:59.235Z" },
3912
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  dependencies = [
225
  { name = "anndata", version = "0.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
226
  { name = "anndata", version = "0.12.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
227
+ { name = "dask", extra = ["distributed"] },
228
  { name = "fastparquet" },
229
  { name = "ipykernel" },
230
  { name = "joblib" },
 
246
  [package.metadata]
247
  requires-dist = [
248
  { name = "anndata", specifier = ">=0.10.0" },
249
+ { name = "dask", extras = ["distributed"], specifier = ">=2026.1.2" },
250
  { name = "fastparquet", specifier = ">=2025.12.0" },
251
  { name = "ipykernel", specifier = ">=6.25.0" },
252
  { name = "joblib", specifier = ">=1.3.0" },
 
456
  { url = "https://files.pythonhosted.org/packages/b7/9f/d73dfb85d7a5b1a56a99adc50f2074029468168c970ff5daeade4ad819e4/choreographer-1.2.1-py3-none-any.whl", hash = "sha256:9af5385effa3c204dbc337abf7ac74fd8908ced326a15645dc31dde75718c77e", size = 49338, upload-time = "2025-11-09T23:04:43.154Z" },
457
  ]
458
 
459
+ [[package]]
460
+ name = "click"
461
+ version = "8.3.1"
462
+ source = { registry = "https://pypi.org/simple" }
463
+ dependencies = [
464
+ { name = "colorama", marker = "sys_platform == 'win32'" },
465
+ ]
466
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
467
+ wheels = [
468
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
469
+ ]
470
+
471
+ [[package]]
472
+ name = "cloudpickle"
473
+ version = "3.1.2"
474
+ source = { registry = "https://pypi.org/simple" }
475
+ sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" }
476
+ wheels = [
477
+ { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" },
478
+ ]
479
+
480
  [[package]]
481
  name = "colorama"
482
  version = "0.4.6"
 
771
  { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
772
  ]
773
 
774
+ [[package]]
775
+ name = "dask"
776
+ version = "2026.1.2"
777
+ source = { registry = "https://pypi.org/simple" }
778
+ dependencies = [
779
+ { name = "click" },
780
+ { name = "cloudpickle" },
781
+ { name = "fsspec" },
782
+ { name = "importlib-metadata", marker = "python_full_version < '3.12'" },
783
+ { name = "packaging" },
784
+ { name = "partd" },
785
+ { name = "pyyaml" },
786
+ { name = "toolz" },
787
+ ]
788
+ sdist = { url = "https://files.pythonhosted.org/packages/bd/52/b0f9172b22778def907db1ff173249e4eb41f054b46a9c83b1528aaf811f/dask-2026.1.2.tar.gz", hash = "sha256:1136683de2750d98ea792670f7434e6c1cfce90cab2cc2f2495a9e60fd25a4fc", size = 10997838, upload-time = "2026-01-30T21:04:20.54Z" }
789
+ wheels = [
790
+ { url = "https://files.pythonhosted.org/packages/e5/23/d39ccc4ed76222db31530b0a7d38876fdb7673e23f838e8d8f0ed4651a4f/dask-2026.1.2-py3-none-any.whl", hash = "sha256:46a0cf3b8d87f78a3d2e6b145aea4418a6d6d606fe6a16c79bd8ca2bb862bc91", size = 1482084, upload-time = "2026-01-30T21:04:18.363Z" },
791
+ ]
792
+
793
+ [package.optional-dependencies]
794
+ distributed = [
795
+ { name = "distributed" },
796
+ ]
797
+
798
  [[package]]
799
  name = "debugpy"
800
  version = "1.8.20"
 
842
  { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
843
  ]
844
 
845
+ [[package]]
846
+ name = "distributed"
847
+ version = "2026.1.2"
848
+ source = { registry = "https://pypi.org/simple" }
849
+ dependencies = [
850
+ { name = "click" },
851
+ { name = "cloudpickle" },
852
+ { name = "dask" },
853
+ { name = "jinja2" },
854
+ { name = "locket" },
855
+ { name = "msgpack" },
856
+ { name = "packaging" },
857
+ { name = "psutil" },
858
+ { name = "pyyaml" },
859
+ { name = "sortedcontainers" },
860
+ { name = "tblib" },
861
+ { name = "toolz" },
862
+ { name = "tornado" },
863
+ { name = "urllib3" },
864
+ { name = "zict" },
865
+ ]
866
+ sdist = { url = "https://files.pythonhosted.org/packages/4e/75/b6e5b77229097ff03dd5ba6a07c77e2da87e7e991ccfef412549bba78746/distributed-2026.1.2.tar.gz", hash = "sha256:8333fa7a34151ed3b4cf1a03136fe1f1799eca706a5e47bdb63022c8795d853b", size = 2103721, upload-time = "2026-01-30T21:07:03.307Z" }
867
+ wheels = [
868
+ { url = "https://files.pythonhosted.org/packages/ad/14/0fe5889a83991ac29c93e6b2e121ad2afc3bff5f9327f34447d3068d8142/distributed-2026.1.2-py3-none-any.whl", hash = "sha256:30ccb5587351f50304f6f6e219ea91bc09d88401125779caa8be5253e9d3ecf2", size = 1009083, upload-time = "2026-01-30T21:07:01.363Z" },
869
+ ]
870
+
871
  [[package]]
872
  name = "donfig"
873
  version = "0.8.1.post1"
 
1201
  { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
1202
  ]
1203
 
1204
+ [[package]]
1205
+ name = "importlib-metadata"
1206
+ version = "8.7.1"
1207
+ source = { registry = "https://pypi.org/simple" }
1208
+ dependencies = [
1209
+ { name = "zipp", marker = "python_full_version < '3.12'" },
1210
+ ]
1211
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
1212
+ wheels = [
1213
+ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
1214
+ ]
1215
+
1216
  [[package]]
1217
  name = "iniconfig"
1218
  version = "2.3.0"
 
1802
  { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" },
1803
  ]
1804
 
1805
+ [[package]]
1806
+ name = "locket"
1807
+ version = "1.0.0"
1808
+ source = { registry = "https://pypi.org/simple" }
1809
+ sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" }
1810
+ wheels = [
1811
+ { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" },
1812
+ ]
1813
+
1814
  [[package]]
1815
  name = "logistro"
1816
  version = "2.0.1"
 
2004
  { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
2005
  ]
2006
 
2007
+ [[package]]
2008
+ name = "msgpack"
2009
+ version = "1.1.2"
2010
+ source = { registry = "https://pypi.org/simple" }
2011
+ sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
2012
+ wheels = [
2013
+ { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" },
2014
+ { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" },
2015
+ { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" },
2016
+ { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" },
2017
+ { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" },
2018
+ { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" },
2019
+ { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" },
2020
+ { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" },
2021
+ { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
2022
+ { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
2023
+ { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
2024
+ { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
2025
+ { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
2026
+ { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
2027
+ { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
2028
+ { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
2029
+ { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
2030
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
2031
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
2032
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
2033
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
2034
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
2035
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
2036
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
2037
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
2038
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
2039
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
2040
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
2041
+ { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
2042
+ { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
2043
+ { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
2044
+ { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
2045
+ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
2046
+ { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
2047
+ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
2048
+ { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
2049
+ { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
2050
+ { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
2051
+ { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
2052
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
2053
+ { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
2054
+ { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
2055
+ { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
2056
+ { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
2057
+ { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
2058
+ { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
2059
+ { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
2060
+ { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
2061
+ { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
2062
+ { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
2063
+ { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
2064
+ { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
2065
+ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
2066
+ ]
2067
+
2068
  [[package]]
2069
  name = "narwhals"
2070
  version = "2.16.0"
 
2596
  { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" },
2597
  ]
2598
 
2599
+ [[package]]
2600
+ name = "partd"
2601
+ version = "1.4.2"
2602
+ source = { registry = "https://pypi.org/simple" }
2603
+ dependencies = [
2604
+ { name = "locket" },
2605
+ { name = "toolz" },
2606
+ ]
2607
+ sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" }
2608
+ wheels = [
2609
+ { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" },
2610
+ ]
2611
+
2612
  [[package]]
2613
  name = "patsy"
2614
  version = "1.0.2"
 
3762
  { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
3763
  ]
3764
 
3765
+ [[package]]
3766
+ name = "sortedcontainers"
3767
+ version = "2.4.0"
3768
+ source = { registry = "https://pypi.org/simple" }
3769
+ sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
3770
+ wheels = [
3771
+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
3772
+ ]
3773
+
3774
  [[package]]
3775
  name = "soupsieve"
3776
  version = "2.8.3"
 
3841
  { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" },
3842
  ]
3843
 
3844
+ [[package]]
3845
+ name = "tblib"
3846
+ version = "3.2.2"
3847
+ source = { registry = "https://pypi.org/simple" }
3848
+ sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/14c15ae154895cc131174f858c707790d416c444fc69f93918adfd8c4c0b/tblib-3.2.2.tar.gz", hash = "sha256:e9a652692d91bf4f743d4a15bc174c0b76afc750fe8c7b6d195cc1c1d6d2ccec", size = 35046, upload-time = "2025-11-12T12:21:16.572Z" }
3849
+ wheels = [
3850
+ { url = "https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", size = 12893, upload-time = "2025-11-12T12:21:14.407Z" },
3851
+ ]
3852
+
3853
  [[package]]
3854
  name = "terminado"
3855
  version = "0.18.1"
 
3939
  { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
3940
  ]
3941
 
3942
+ [[package]]
3943
+ name = "toolz"
3944
+ version = "1.1.0"
3945
+ source = { registry = "https://pypi.org/simple" }
3946
+ sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" }
3947
+ wheels = [
3948
+ { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" },
3949
+ ]
3950
+
3951
  [[package]]
3952
  name = "tornado"
3953
  version = "6.5.4"
 
4105
  wheels = [
4106
  { url = "https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl", hash = "sha256:29cd905afb6235b94c09decda4258c888fcb79bb6c862ef7c0b8fe009b5c8563", size = 284067, upload-time = "2025-11-21T14:05:59.235Z" },
4107
  ]
4108
+
4109
+ [[package]]
4110
+ name = "zict"
4111
+ version = "3.0.0"
4112
+ source = { registry = "https://pypi.org/simple" }
4113
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/ac/3c494dd7ec5122cff8252c1a209b282c0867af029f805ae9befd73ae37eb/zict-3.0.0.tar.gz", hash = "sha256:e321e263b6a97aafc0790c3cfb3c04656b7066e6738c37fffcca95d803c9fba5", size = 33238, upload-time = "2023-04-17T21:41:16.041Z" }
4114
+ wheels = [
4115
+ { url = "https://files.pythonhosted.org/packages/80/ab/11a76c1e2126084fde2639514f24e6111b789b0bfa4fc6264a8975c7e1f1/zict-3.0.0-py2.py3-none-any.whl", hash = "sha256:5796e36bd0e0cc8cf0fbc1ace6a68912611c1dbd74750a3f3026b9b9d6a327ae", size = 43332, upload-time = "2023-04-17T21:41:13.444Z" },
4116
+ ]
4117
+
4118
+ [[package]]
4119
+ name = "zipp"
4120
+ version = "3.23.0"
4121
+ source = { registry = "https://pypi.org/simple" }
4122
+ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
4123
+ wheels = [
4124
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
4125
+ ]