extract-tracker 0.2.3__tar.gz → 0.3.0__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 (58) hide show
  1. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/PKG-INFO +1 -1
  2. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/pyproject.toml +1 -1
  3. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract_tracker.egg-info/PKG-INFO +1 -1
  4. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/Cargo.lock +1 -1
  5. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/Cargo.toml +1 -1
  6. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/app.rs +2 -0
  7. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/db.rs +25 -0
  8. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/layout.rs +8 -1
  9. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/popup.rs +121 -13
  10. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/summary.rs +9 -1
  11. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/MANIFEST.in +0 -0
  12. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/README.md +0 -0
  13. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/manual/config.md +0 -0
  14. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/manual/mcp.md +0 -0
  15. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/manual/packaging.md +0 -0
  16. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/manual/sync.md +0 -0
  17. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/manual/tui.md +0 -0
  18. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/manual/usage.md +0 -0
  19. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/__init__.py +0 -0
  20. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/__main__.py +0 -0
  21. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/cli_query.py +0 -0
  22. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/experiment.py +0 -0
  23. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/init.py +0 -0
  24. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/mcp.py +0 -0
  25. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/metrics.py +0 -0
  26. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/release_versioning.py +0 -0
  27. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/run.py +0 -0
  28. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/store.py +0 -0
  29. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract/sync.py +0 -0
  30. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract_tracker.egg-info/SOURCES.txt +0 -0
  31. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract_tracker.egg-info/dependency_links.txt +0 -0
  32. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract_tracker.egg-info/entry_points.txt +0 -0
  33. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract_tracker.egg-info/requires.txt +0 -0
  34. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/python/src/extract_tracker.egg-info/top_level.txt +0 -0
  35. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/artifact.rs +0 -0
  36. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/config.rs +0 -0
  37. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/event.rs +0 -0
  38. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/keys.rs +0 -0
  39. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/main.rs +0 -0
  40. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/model.rs +0 -0
  41. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/chart.rs +0 -0
  42. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/compare.rs +0 -0
  43. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/dashboard.rs +0 -0
  44. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/detail.rs +0 -0
  45. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/diff.rs +0 -0
  46. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/heatmap.rs +0 -0
  47. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/help.rs +0 -0
  48. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/lineage.rs +0 -0
  49. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/mod.rs +0 -0
  50. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/registry.rs +0 -0
  51. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/search.rs +0 -0
  52. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/selection.rs +0 -0
  53. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/statusbar.rs +0 -0
  54. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/theme.rs +0 -0
  55. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/todo.rs +0 -0
  56. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/rust/src/ui/tree.rs +0 -0
  57. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/setup.cfg +0 -0
  58. {extract_tracker-0.2.3 → extract_tracker-0.3.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: extract-tracker
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Local-first experiment tracking for deep learning
5
5
  Author: Phil Oh
6
6
  Keywords: experiment-tracking,machine-learning,deep-learning,sqlite,tui,mcp
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "extract-tracker"
7
- version = "0.2.3"
7
+ version = "0.3.0"
8
8
  description = "Local-first experiment tracking for deep learning"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: extract-tracker
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Local-first experiment tracking for deep learning
5
5
  Author: Phil Oh
6
6
  Keywords: experiment-tracking,machine-learning,deep-learning,sqlite,tui,mcp
@@ -592,7 +592,7 @@ dependencies = [
592
592
 
593
593
  [[package]]
594
594
  name = "extract-tui"
595
- version = "0.2.3"
595
+ version = "0.3.0"
596
596
  dependencies = [
597
597
  "chrono",
598
598
  "clap",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "extract-tui"
3
- version = "0.2.3"
3
+ version = "0.3.0"
4
4
  edition = "2024"
5
5
 
6
6
  [dependencies]
@@ -306,6 +306,7 @@ pub struct RunBrowserState {
306
306
  pub filtered: Vec<usize>,
307
307
  pub cursor: usize,
308
308
  pub search_query: Option<String>,
309
+ pub rename_buffer: Option<String>,
309
310
  pub scroll_offset: usize,
310
311
  }
311
312
 
@@ -318,6 +319,7 @@ impl RunBrowserState {
318
319
  filtered,
319
320
  cursor: 0,
320
321
  search_query: None,
322
+ rename_buffer: None,
321
323
  scroll_offset: 0,
322
324
  }
323
325
  }
@@ -854,6 +854,19 @@ impl Db {
854
854
  Ok(())
855
855
  }
856
856
 
857
+ /// Rename a run. Empty string clears the name. Opens a writable connection.
858
+ pub fn rename_run(db_path: &Path, id: &str, name: &str) -> Result<()> {
859
+ let conn = Connection::open(db_path)?;
860
+ conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
861
+ let trimmed = name.trim();
862
+ let value: Option<&str> = if trimmed.is_empty() { None } else { Some(trimmed) };
863
+ conn.execute(
864
+ "UPDATE runs SET name = ? WHERE id = ?",
865
+ params![value, id],
866
+ )?;
867
+ Ok(())
868
+ }
869
+
857
870
  /// Set status on a single run or experiment. Opens a writable connection.
858
871
  pub fn set_status(db_path: &Path, table: &str, id: &str, status: &str) -> Result<()> {
859
872
  assert!(table == "runs" || table == "experiments");
@@ -1314,6 +1327,18 @@ mod tests {
1314
1327
  assert_eq!(run.status, "failed");
1315
1328
  }
1316
1329
 
1330
+ #[test]
1331
+ fn test_rename_run() {
1332
+ let tdb = test_db_with_path();
1333
+ Db::rename_run(&tdb.path, "r1", "renamed").unwrap();
1334
+ let run = tdb.db.get_run("r1").unwrap().unwrap();
1335
+ assert_eq!(run.name.as_deref(), Some("renamed"));
1336
+
1337
+ Db::rename_run(&tdb.path, "r1", " ").unwrap();
1338
+ let run = tdb.db.get_run("r1").unwrap().unwrap();
1339
+ assert!(run.name.is_none());
1340
+ }
1341
+
1317
1342
  #[test]
1318
1343
  fn test_archive_experiment() {
1319
1344
  let tdb = test_db_with_path();
@@ -82,7 +82,14 @@ impl AppLayout {
82
82
  // focused panel's input handler see every keystroke unmodified.
83
83
  let in_text_input = state.tag_picker.is_some()
84
84
  || state.note_input.is_some()
85
- || state.todo_input.is_some();
85
+ || state.todo_input.is_some()
86
+ || state
87
+ .run_picker
88
+ .as_ref()
89
+ .is_some_and(|picker| picker.search_query.is_some())
90
+ || state.run_browser.as_ref().is_some_and(|browser| {
91
+ browser.search_query.is_some() || browser.rename_buffer.is_some()
92
+ });
86
93
 
87
94
  if !in_text_input {
88
95
  // Global keys: gg/G, ?, work in all views
@@ -1,4 +1,5 @@
1
- use crossterm::event::{KeyCode, KeyEvent};
1
+ use chrono::{DateTime, Local};
2
+ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2
3
  use ratatui::layout::Rect;
3
4
  use ratatui::style::{Modifier, Style};
4
5
  use ratatui::symbols::border;
@@ -216,6 +217,70 @@ impl PopupRenderer {
216
217
  };
217
218
 
218
219
  let is_searching = browser.search_query.is_some();
220
+ let is_renaming = browser.rename_buffer.is_some();
221
+
222
+ // Rename mode
223
+ if is_renaming {
224
+ match key.code {
225
+ KeyCode::Esc => {
226
+ browser.rename_buffer = None;
227
+ return false;
228
+ }
229
+ KeyCode::Enter => {
230
+ if let Some(&run_idx) = browser.filtered.get(browser.cursor) {
231
+ if let Some(run) = browser.runs.get(run_idx) {
232
+ let run_id = run.id.clone();
233
+ let new_name = browser.rename_buffer.take().unwrap_or_default();
234
+ let db_path = state.store_root.join("extract.db");
235
+ match crate::db::Db::rename_run(&db_path, &run_id, &new_name) {
236
+ Ok(()) => {
237
+ let trimmed = new_name.trim();
238
+ let value = if trimmed.is_empty() {
239
+ None
240
+ } else {
241
+ Some(trimmed.to_string())
242
+ };
243
+ if let Some(run) = browser.runs.get_mut(run_idx) {
244
+ run.name = value.clone();
245
+ }
246
+ if let Some(state_idx) = state.runs.iter().position(|r| r.id == run_id) {
247
+ state.runs[state_idx].name = value;
248
+ }
249
+ let _ = state.refresh_selection_summary();
250
+ state.notify(crate::app::NotifyLevel::Success, "Run renamed");
251
+ }
252
+ Err(err) => {
253
+ state.notify(
254
+ crate::app::NotifyLevel::Error,
255
+ format!("Rename failed: {err}"),
256
+ );
257
+ }
258
+ }
259
+ } else {
260
+ browser.rename_buffer = None;
261
+ }
262
+ } else {
263
+ browser.rename_buffer = None;
264
+ }
265
+ return false;
266
+ }
267
+ KeyCode::Backspace => {
268
+ if let Some(ref mut name) = browser.rename_buffer {
269
+ name.pop();
270
+ }
271
+ return false;
272
+ }
273
+ KeyCode::Char(c) => {
274
+ if accepts_text_modifiers(key) {
275
+ if let Some(ref mut name) = browser.rename_buffer {
276
+ name.push(c);
277
+ }
278
+ }
279
+ return false;
280
+ }
281
+ _ => return false,
282
+ }
283
+ }
219
284
 
220
285
  // Search mode
221
286
  if is_searching {
@@ -290,6 +355,15 @@ impl PopupRenderer {
290
355
  return false;
291
356
  }
292
357
 
358
+ if is_rename_key(key) {
359
+ if let Some(&run_idx) = browser.filtered.get(browser.cursor) {
360
+ if let Some(run) = browser.runs.get(run_idx) {
361
+ browser.rename_buffer = Some(run.name.clone().unwrap_or_default());
362
+ }
363
+ }
364
+ return false;
365
+ }
366
+
293
367
  if keys::matches(key, keys::SELECT) {
294
368
  if let Some(&filtered_idx) = browser.filtered.get(browser.cursor) {
295
369
  if let Some(run) = browser.runs.get(filtered_idx) {
@@ -499,6 +573,7 @@ impl PopupRenderer {
499
573
  browser: &mut RunBrowserState,
500
574
  ) {
501
575
  let is_searching = browser.search_query.is_some();
576
+ let is_renaming = browser.rename_buffer.is_some();
502
577
  let width = POPUP_WIDTH.min(area.width.saturating_sub(4));
503
578
  let height = POPUP_HEIGHT.min(area.height.saturating_sub(4));
504
579
  let popup_area = centered_rect(width, height, area);
@@ -508,12 +583,16 @@ impl PopupRenderer {
508
583
  let title = format!(" {} — runs ", browser.experiment_name);
509
584
  let footer_spans = if is_searching {
510
585
  search_footer_spans(&self.theme)
586
+ } else if is_renaming {
587
+ rename_footer_spans(&self.theme)
511
588
  } else {
512
589
  vec![
513
590
  Span::styled("j/k", Style::default().fg(self.theme.accent)),
514
591
  Span::styled(" nav ", Style::default().fg(self.theme.accent_dim)),
515
592
  Span::styled("Enter", Style::default().fg(self.theme.accent)),
516
593
  Span::styled(" select ", Style::default().fg(self.theme.accent_dim)),
594
+ Span::styled("R", Style::default().fg(self.theme.accent)),
595
+ Span::styled(" rename ", Style::default().fg(self.theme.accent_dim)),
517
596
  Span::styled("/", Style::default().fg(self.theme.accent)),
518
597
  Span::styled(" search ", Style::default().fg(self.theme.accent_dim)),
519
598
  Span::styled("x", Style::default().fg(self.theme.accent)),
@@ -570,11 +649,19 @@ impl PopupRenderer {
570
649
  Style::default()
571
650
  };
572
651
 
573
- let label = run
574
- .name
575
- .as_deref()
576
- .map(|n| format!("{} ", n))
577
- .unwrap_or_default();
652
+ let label = if is_cursor {
653
+ browser
654
+ .rename_buffer
655
+ .as_ref()
656
+ .map(|name| format!("{name}_ "))
657
+ .or_else(|| run.name.as_deref().map(|n| format!("{} ", n)))
658
+ .unwrap_or_default()
659
+ } else {
660
+ run.name
661
+ .as_deref()
662
+ .map(|n| format!("{} ", n))
663
+ .unwrap_or_default()
664
+ };
578
665
  let date = format_date(run);
579
666
  let config_summary = differing_config_summary(run, &diff_keys);
580
667
 
@@ -624,14 +711,24 @@ fn compute_scroll(cursor: usize, current_offset: usize, list_height: usize) -> u
624
711
  }
625
712
  }
626
713
 
627
- /// Format a run's date for display (first 19 chars of ended_at or started_at).
714
+ /// Format a run timestamp for display in the user's local timezone.
628
715
  fn format_date(run: &Run) -> String {
629
- run.ended_at
630
- .as_deref()
631
- .unwrap_or(&run.started_at)
632
- .chars()
633
- .take(19)
634
- .collect()
716
+ let raw = run.ended_at.as_deref().unwrap_or(&run.started_at);
717
+ format_local_timestamp(raw).unwrap_or_else(|| raw.chars().take(19).collect())
718
+ }
719
+
720
+ fn format_local_timestamp(raw: &str) -> Option<String> {
721
+ DateTime::parse_from_rfc3339(raw)
722
+ .ok()
723
+ .map(|dt| dt.with_timezone(&Local).format("%Y-%m-%d %H:%M:%S").to_string())
724
+ }
725
+
726
+ fn is_rename_key(key: &KeyEvent) -> bool {
727
+ matches!(key.code, KeyCode::Char('R')) && accepts_text_modifiers(key)
728
+ }
729
+
730
+ fn accepts_text_modifiers(key: &KeyEvent) -> bool {
731
+ key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT
635
732
  }
636
733
 
637
734
  /// Build search input line with blinking cursor.
@@ -655,6 +752,17 @@ fn search_footer_spans(theme: &Theme) -> Vec<Span<'static>> {
655
752
  ]
656
753
  }
657
754
 
755
+ fn rename_footer_spans(theme: &Theme) -> Vec<Span<'static>> {
756
+ vec![
757
+ Span::styled("Type", Style::default().fg(theme.accent)),
758
+ Span::styled(" name ", Style::default().fg(theme.accent_dim)),
759
+ Span::styled("Enter", Style::default().fg(theme.accent)),
760
+ Span::styled(" save ", Style::default().fg(theme.accent_dim)),
761
+ Span::styled("Esc", Style::default().fg(theme.accent)),
762
+ Span::styled(" cancel", Style::default().fg(theme.accent_dim)),
763
+ ]
764
+ }
765
+
658
766
  /// Compare configs across all runs and return only the keys whose values differ.
659
767
  fn differing_config_keys(runs: &[Run]) -> Vec<String> {
660
768
  use std::collections::HashMap;
@@ -1,3 +1,4 @@
1
+ use chrono::{DateTime, Local};
1
2
  use ratatui::buffer::Buffer;
2
3
  use ratatui::layout::Rect;
3
4
  use ratatui::style::{Color, Modifier, Style};
@@ -219,7 +220,7 @@ impl SummaryRenderer {
219
220
  }
220
221
 
221
222
  let run = &data.runs[i];
222
- let date = run.started_at.get(..10).unwrap_or(&run.started_at);
223
+ let date = format_run_time(run);
223
224
  let label = run.name.clone().unwrap_or_else(|| {
224
225
  let id = &run.id;
225
226
  if id.len() > 8 { id[id.len() - 8..].to_string() } else { id.clone() }
@@ -632,6 +633,13 @@ impl SummaryRenderer {
632
633
  }
633
634
  }
634
635
 
636
+ fn format_run_time(run: &Run) -> String {
637
+ DateTime::parse_from_rfc3339(&run.started_at)
638
+ .ok()
639
+ .map(|dt| dt.with_timezone(&Local).format("%Y-%m-%d %H:%M:%S").to_string())
640
+ .unwrap_or_else(|| run.started_at.chars().take(19).collect())
641
+ }
642
+
635
643
  /// Catmull-Rom spline interpolation.
636
644
  /// Generates `num_points` evenly spaced points along the spline that passes
637
645
  /// through all input points. Requires at least 3 input points.