Print stats
This commit is contained in:
1 parent
ef2074f65b
commit
d38be55598
9 files changed
+296
-6
No files matched your search
Generated
+123
@@ -11,6 +11,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
@@ -23,6 +32,12 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.9"
|
||||
@@ -130,6 +145,20 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
@@ -142,6 +171,12 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
@@ -420,6 +455,30 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -528,6 +587,15 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -641,6 +709,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"hmac",
|
||||
"secrecy",
|
||||
@@ -665,6 +734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"chrono",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
@@ -1155,12 +1225,65 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.104", features = ["backtrace"] }
|
||||
axum = { version = "0.8.9", features = ["macros"] }
|
||||
chrono = { version = "0.4.45", features = ["serde"] }
|
||||
dotenvy = "0.15.7"
|
||||
hmac = "0.13.0"
|
||||
secrecy = { version = "0.10.3", features = ["serde"] }
|
||||
@@ -15,7 +16,7 @@ sha2 = "0.11.0"
|
||||
tempfile = "3.27.0"
|
||||
thiserror = "2.0.21"
|
||||
tokio = { version = "1.53.2", features = ["full"] }
|
||||
tokio-rusqlite = { version = "0.8.0", features = ["bundled", "uuid"] }
|
||||
tokio-rusqlite = { version = "0.8.0", features = ["bundled", "uuid", "chrono"] }
|
||||
tower = { version = "0.5.3", features = ["tracing"] }
|
||||
tower-http = { version = "0.7.1", features = ["trace", "request-id"] }
|
||||
tracing = "0.1.44"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod users;
|
||||
pub mod syncs;
|
||||
pub mod stats;
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use axum::{Json, Router, extract::State, routing};
|
||||
use chrono::NaiveDate;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{app::AppState, app_error::AppError, auth_user::AuthUser};
|
||||
|
||||
pub fn stats_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/contributions", routing::get(get_contributions))
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResponseContribution {
|
||||
date: NaiveDate,
|
||||
document: String,
|
||||
progress: f32,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(st))]
|
||||
pub async fn get_contributions(State(st) : State<AppState>, AuthUser{username} : AuthUser) -> Result<Json<Vec<ResponseContribution>>, AppError> {
|
||||
let cs = st.stats
|
||||
.get_contributions(&username).await?
|
||||
.into_iter().map(|c| ResponseContribution{date: c.date, document: c.document_id, progress: c.progress})
|
||||
.collect();
|
||||
Ok(Json(cs))
|
||||
}
|
||||
+4
-2
@@ -2,12 +2,13 @@ use std::{path::Path, sync::Arc};
|
||||
|
||||
use axum::extract::FromRef;
|
||||
|
||||
use crate::{config::Config, reader::Reader, repository::{progress_sync::ProgressSync, users::Users}, writer::Writer};
|
||||
use crate::{config::Config, reader::Reader, repository::{progress_sync::ProgressSync, stats::Stats, users::Users}, writer::Writer};
|
||||
|
||||
#[derive(Clone, FromRef)]
|
||||
pub struct AppState {
|
||||
pub users: Users,
|
||||
pub sync: ProgressSync,
|
||||
pub stats: Stats,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -16,7 +17,8 @@ impl AppState {
|
||||
let reader = Arc::new(Reader::new(&db_path).await?);
|
||||
let users = Users::new(writer.clone(), reader.clone(), config.server_secret.clone());
|
||||
let sync = ProgressSync::new(writer.clone(), reader.clone());
|
||||
Ok(Self{users, sync})
|
||||
let stats = Stats::new(reader.clone());
|
||||
Ok(Self{users, sync, stats})
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -1,7 +1,7 @@
|
||||
use axum::{Json, http::StatusCode, response::IntoResponse};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::repository::{progress_sync::SyncError, users::UserError};
|
||||
use crate::repository::{progress_sync::SyncError, stats::StatsError, users::UserError};
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@ impl From<SyncError> for AppError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StatsError> for AppError {
|
||||
fn from(value: StatsError) -> Self {
|
||||
match value {
|
||||
StatsError::DatabaseFailure(error) => Self::Internal(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let (status, error_code, msg) = match self {
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
use axum::{Json, Router, response::{IntoResponse as _, Response}, routing};
|
||||
use rukosync::{api::{syncs, users}, app::AppState, config::Config, };
|
||||
use rukosync::{api::{stats, syncs, users}, app::AppState, config::Config, };
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _};
|
||||
@@ -8,7 +8,7 @@ use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt as _, util::Subscr
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let level_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::DEBUG.into())
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer())
|
||||
@@ -25,6 +25,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/healthcheck", routing::get(healthcheck))
|
||||
.nest("/users", users::users_routes())
|
||||
.nest("/syncs", syncs::syncs_routes())
|
||||
.nest("/stats", stats::stats_routes())
|
||||
.with_state(app_state)
|
||||
.layer(TraceLayer::new_for_http());
|
||||
let address = format!("0.0.0.0:{}", config.port);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod progress_sync;
|
||||
pub mod users;
|
||||
pub mod stats;
|
||||
@@ -0,0 +1,126 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use thiserror::Error;
|
||||
use tokio_rusqlite::{named_params, rusqlite};
|
||||
|
||||
use crate::reader::Reader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stats {
|
||||
reader: Arc<Reader>,
|
||||
}
|
||||
|
||||
// Github style contributions
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Contribution {
|
||||
pub date: NaiveDate,
|
||||
pub start_percentage: f32,
|
||||
pub end_percentage: f32,
|
||||
pub progress: f32,
|
||||
pub document_id: String,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum StatsError {
|
||||
#[error("Database call failed {0}")]
|
||||
DatabaseFailure(#[from] tokio_rusqlite::Error)
|
||||
}
|
||||
|
||||
impl Stats {
|
||||
pub fn new(reader: Arc<Reader>) -> Self {
|
||||
Self{reader}
|
||||
}
|
||||
|
||||
pub async fn get_contributions(&self, username : &str) -> Result<Vec<Contribution>, StatsError> {
|
||||
let username = username.to_owned();
|
||||
let cs = self.reader.connection.call(move |conn| -> rusqlite::Result<Vec<Contribution>> {
|
||||
let mut stmt = conn.prepare("select
|
||||
date(progress.timestamp, 'unixepoch') as day,
|
||||
progress.document_id,
|
||||
min(progress.percentage) as start_percentage,
|
||||
max(progress.percentage) as end_percentage,
|
||||
max(progress.percentage) - min(progress.percentage) as progress
|
||||
from progress
|
||||
join users on users.id = progress.user_id
|
||||
where users.username = :username
|
||||
group by day, progress.document_id
|
||||
order by day")?;
|
||||
let x = stmt.query_map(named_params!{":username": username}, |row| {
|
||||
let date = row.get(0)?;
|
||||
let document_id = row.get(1)?;
|
||||
let start_percentage = row.get(2)?;
|
||||
let end_percentage = row.get(3)?;
|
||||
let progress = row.get(4)?;
|
||||
Ok(Contribution{date, start_percentage, end_percentage, progress, document_id})
|
||||
})?.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
Ok(x)
|
||||
}).await?;
|
||||
Ok(cs)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use secrecy::SecretString;
|
||||
use tempfile::{TempDir, tempdir};
|
||||
|
||||
use crate::{reader::Reader, repository::{progress_sync::{Document, ProgressSync}, stats::Stats, users::Users}, writer::Writer};
|
||||
use super::*;
|
||||
|
||||
const USERNAME : &str = "foo@example.com";
|
||||
|
||||
async fn create_repositories() -> anyhow::Result<(TempDir, ProgressSync, Stats)> {
|
||||
let temp_dir = tempdir()?;
|
||||
let path = temp_dir.path().join("kosync.db");
|
||||
let writer = Arc::new(Writer::new(&path).await?);
|
||||
let reader = Arc::new(Reader::new(&path).await?);
|
||||
let users = Users::new(writer.clone(), reader.clone(), SecretString::from("secret"));
|
||||
let sync = ProgressSync::new(writer.clone(), reader.clone());
|
||||
let stats = Stats::new(reader.clone());
|
||||
users.create_user(USERNAME, SecretString::from("password")).await?;
|
||||
Ok((temp_dir, sync, stats))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_progress() -> anyhow::Result<()> {
|
||||
let (_t, _, stats) = create_repositories().await?;
|
||||
let contributions = stats.get_contributions(USERNAME).await?;
|
||||
assert_eq!(contributions, vec![]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn now() -> i64 {
|
||||
chrono::Utc::now().timestamp()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_read() -> anyhow::Result<()> {
|
||||
let (_t, sync, stats) = create_repositories().await?;
|
||||
let document_id = "foo".to_string();
|
||||
let doc1 = Document {
|
||||
document_id: document_id.clone(),
|
||||
progress: "prog".to_string(),
|
||||
percentage: 0.5,
|
||||
device: "kobo".to_string(),
|
||||
timestamp: now(),
|
||||
filename: Some("foo.epub".to_string()),
|
||||
title: Some("A Book".to_string()),
|
||||
author: Some("A.N. Author".to_string()),
|
||||
..Document::default()
|
||||
};
|
||||
sync.set_progress(USERNAME, &doc1).await?;
|
||||
let contributions = stats.get_contributions(USERNAME).await?;
|
||||
let today = chrono::Utc::now().date_naive();
|
||||
let wanted = Contribution {
|
||||
date: today,
|
||||
start_percentage: 0.5,
|
||||
end_percentage: 0.5,
|
||||
progress: 0.0,
|
||||
document_id: document_id.clone()
|
||||
};
|
||||
assert_eq!(contributions, vec![wanted]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user