Skip to content

Commit

Permalink
Add timetracker-dump program.
Browse files Browse the repository at this point in the history
This tool is intended to be used for debugging, allowing a person to
view the data in the database in a human-readable format.
  • Loading branch information
david-cattermole committed Sep 11, 2023
1 parent 1eb2897 commit ab4e36b
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 0 deletions.
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ authors = ["David Cattermole"]
members = [
"configure-bin",
"core",
"dump-bin",
"print-bin",
"print-lib",
"recorder-bin",
Expand Down
1 change: 1 addition & 0 deletions dump-bin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
25 changes: 25 additions & 0 deletions dump-bin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "timetracker-dump"
description = "Dumps Timetracker data to stdout or a file."
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true

[dependencies]
anyhow = "1.0.70"
chrono = "0.4.19"
clap = { version = "3.2.23", features = ["std", "derive"], default-features = false }
colored = { version = "2.0.0", default-features = true }
config = { version = "0.13.3", features = ["toml"], default-features = false }
dirs = "4.0.0"
env_logger = "0.10.0"
log = "0.4.17"
serde = "1.0.159"
serde_derive = "1.0.159"

[dependencies.timetracker-core]
path = "../core"

[dependencies.timetracker-print-lib]
path = "../print-lib"
14 changes: 14 additions & 0 deletions dump-bin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Dump (binary)

This directory contains the Rust crate for the main Dump program.

Dump will gather data from the (database) storage then write that data
to stdout or to a file.

## Configuration

To be written.

## How Dump Works

To be written.
144 changes: 144 additions & 0 deletions dump-bin/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use crate::settings::CommandArguments;
use crate::settings::DumpAppSettings;
use anyhow::bail;
use anyhow::Result;
use clap::Parser;
use log::debug;
use std::time::SystemTime;
use timetracker_core::filesystem::get_database_file_path;
use timetracker_core::settings::RECORD_INTERVAL_SECONDS;
use timetracker_core::storage::Storage;
use timetracker_print_lib::datetime::DateTimeLocalPair;
use timetracker_print_lib::print::get_relative_week_start_end;

mod settings;

// CSV Spec: Each record is located on a separate line,
// delimited by a line break (CRLF).
static LINE_END: &str = "\r\n";

fn convert_to_csv_string_value(entry_var_name: &Option<String>) -> String {
match &entry_var_name {
Some(value) => value.to_string(),
None => "".to_string(),
}
}

fn generate_csv_formated_lines(
storage: &mut Storage,
lines: &mut Vec<String>,
week_datetime_pair: DateTimeLocalPair,
) -> Result<()> {
let (week_start_datetime, week_end_datetime) = week_datetime_pair;

let week_start_of_time = week_start_datetime.timestamp() as u64;
let week_end_of_time = week_end_datetime.timestamp() as u64;
let week_entries = storage.read_entries(week_start_of_time, week_end_of_time)?;

for week_entry in week_entries {
let line = format!(
concat!(
"{utc_time_seconds},{duration_seconds},",
"{status:?},{executable},",
"{var1_name},{var1_value},",
"{var2_name},{var2_value},",
"{var3_name},{var3_value},",
"{var4_name},{var4_value},",
"{var5_name},{var5_value}"
),
utc_time_seconds = week_entry.utc_time_seconds,
duration_seconds = week_entry.duration_seconds,
status = week_entry.status,
executable = convert_to_csv_string_value(&week_entry.vars.executable),
var1_name = convert_to_csv_string_value(&week_entry.vars.var1_name),
var1_value = convert_to_csv_string_value(&week_entry.vars.var1_value),
var2_name = convert_to_csv_string_value(&week_entry.vars.var2_name),
var2_value = convert_to_csv_string_value(&week_entry.vars.var2_value),
var3_name = convert_to_csv_string_value(&week_entry.vars.var3_name),
var3_value = convert_to_csv_string_value(&week_entry.vars.var3_value),
var4_name = convert_to_csv_string_value(&week_entry.vars.var4_name),
var4_value = convert_to_csv_string_value(&week_entry.vars.var4_value),
var5_name = convert_to_csv_string_value(&week_entry.vars.var5_name),
var5_value = convert_to_csv_string_value(&week_entry.vars.var5_value),
);
lines.push(line);
}
Ok(())
}

fn dump_database(
args: &CommandArguments,
settings: &DumpAppSettings,
output_lines: &mut Vec<String>,
) -> Result<()> {
let database_file_path = get_database_file_path(
&settings.core.database_dir,
&settings.core.database_file_name,
);

let mut storage = Storage::open_as_read_only(
&database_file_path.expect("Database file path should be valid"),
RECORD_INTERVAL_SECONDS,
)?;

let relative_week = if args.last_week {
-1
} else {
args.relative_week
};

// 'relative_week' is added to the week number to find. A value of
// '-1' will get the previous week, a value of '0' will get the
// current week, and a value of '1' will get the next week (which
// shouldn't really give any results, so it's probably pointless).
let week_datetime_pair = get_relative_week_start_end(relative_week)?;

generate_csv_formated_lines(&mut storage, output_lines, week_datetime_pair)
}

fn main() -> Result<()> {
let env = env_logger::Env::default()
.filter_or("TIMETRACKER_LOG", "warn")
.write_style("TIMETRACKER_LOG_STYLE");
env_logger::init_from_env(env);

let args = CommandArguments::parse();

let settings = DumpAppSettings::new(&args);
if settings.is_err() {
bail!("Settings are invalid: {:?}", settings);
}
let settings = settings?;
debug!("Settings validated: {:#?}", settings);

let now = SystemTime::now();

let mut lines = Vec::new();
dump_database(&args, &settings, &mut lines)?;

if !lines.is_empty() {
// The CSV File Format header is described here:
// https://www.rfc-editor.org/rfc/rfc4180#section-2
print!(
"{}{}",
concat!(
"utc_time_seconds,duration_seconds,",
"status,executable,",
"var1_name,var1_value,",
"var2_name,var2_value,",
"var3_name,var3_value,",
"var4_name,var4_value,",
"var5_name,var5_value",
),
LINE_END
);
for line in &lines {
print!("{}{}", line, LINE_END);
}
}

let duration = now.elapsed()?.as_secs_f32();
debug!("Time taken: {:.2} seconds", duration);

Ok(())
}
53 changes: 53 additions & 0 deletions dump-bin/src/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use clap::Parser;
use config::ConfigError;
use serde_derive::Deserialize;
use timetracker_core::settings::new_core_settings;
use timetracker_core::settings::new_print_settings;
use timetracker_core::settings::validate_core_settings;
use timetracker_core::settings::CoreSettings;
use timetracker_core::settings::PrintSettings;

#[derive(Parser, Debug)]
#[clap(author = "David Cattermole, Copyright 2023", version, about)]
pub struct CommandArguments {
/// Return the last week's results, shortcut for
/// '--relative-week=-1'.
#[clap(long, value_parser, default_value_t = false)]
pub last_week: bool,

/// Relative week number. '0' is the current week, '-1' is the
/// previous week, etc.
#[clap(short = 'w', long, value_parser, default_value_t = 0)]
pub relative_week: i32,

/// Override the directory to search for the database file.
#[clap(long, value_parser)]
pub database_dir: Option<String>,

/// Override the name of the database file to open.
#[clap(long, value_parser)]
pub database_file_name: Option<String>,
}

#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct DumpAppSettings {
pub core: CoreSettings,
pub print: PrintSettings,
}

impl DumpAppSettings {
pub fn new(arguments: &CommandArguments) -> Result<Self, ConfigError> {
let builder = new_core_settings(
arguments.database_dir.clone(),
arguments.database_file_name.clone(),
false,
)?;
let builder = new_print_settings(builder)?;

let settings: Self = builder.build()?.try_deserialize()?;
validate_core_settings(&settings.core).unwrap();

Ok(settings)
}
}

0 comments on commit ab4e36b

Please sign in to comment.