Sync your own workout data from your "Strong" app
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fixed data formats for clickhouse

+202 -117
+111 -63
Cargo.lock
··· 18 18 checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 19 20 20 [[package]] 21 - name = "android-tzdata" 22 - version = "0.1.1" 23 - source = "registry+https://github.com/rust-lang/crates.io-index" 24 - checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 - 26 - [[package]] 27 - name = "android_system_properties" 28 - version = "0.1.5" 29 - source = "registry+https://github.com/rust-lang/crates.io-index" 30 - checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 - dependencies = [ 32 - "libc", 33 - ] 34 - 35 - [[package]] 36 21 name = "atomic-waker" 37 22 version = "1.1.2" 38 23 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 108 93 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 109 94 110 95 [[package]] 111 - name = "chrono" 112 - version = "0.4.40" 113 - source = "registry+https://github.com/rust-lang/crates.io-index" 114 - checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 115 - dependencies = [ 116 - "android-tzdata", 117 - "iana-time-zone", 118 - "js-sys", 119 - "num-traits", 120 - "wasm-bindgen", 121 - "windows-link", 122 - ] 123 - 124 - [[package]] 125 96 name = "cityhash-rs" 126 97 version = "1.0.1" 127 98 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 143 114 "hyper", 144 115 "hyper-util", 145 116 "lz4_flex", 117 + "quanta", 146 118 "replace_with", 147 119 "sealed", 148 120 "serde", 149 121 "static_assertions", 150 122 "thiserror", 123 + "time", 151 124 "tokio", 152 125 "url", 126 + "uuid", 153 127 ] 154 128 155 129 [[package]] ··· 181 155 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 182 156 183 157 [[package]] 158 + name = "crossbeam-utils" 159 + version = "0.8.21" 160 + source = "registry+https://github.com/rust-lang/crates.io-index" 161 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 162 + 163 + [[package]] 164 + name = "deranged" 165 + version = "0.4.0" 166 + source = "registry+https://github.com/rust-lang/crates.io-index" 167 + checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 168 + dependencies = [ 169 + "powerfmt", 170 + ] 171 + 172 + [[package]] 184 173 name = "displaydoc" 185 174 version = "0.2.5" 186 175 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 521 510 ] 522 511 523 512 [[package]] 524 - name = "iana-time-zone" 525 - version = "0.1.61" 526 - source = "registry+https://github.com/rust-lang/crates.io-index" 527 - checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 528 - dependencies = [ 529 - "android_system_properties", 530 - "core-foundation-sys", 531 - "iana-time-zone-haiku", 532 - "js-sys", 533 - "wasm-bindgen", 534 - "windows-core", 535 - ] 536 - 537 - [[package]] 538 - name = "iana-time-zone-haiku" 539 - version = "0.1.2" 540 - source = "registry+https://github.com/rust-lang/crates.io-index" 541 - checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 542 - dependencies = [ 543 - "cc", 544 - ] 545 - 546 - [[package]] 547 513 name = "icu_collections" 548 514 version = "1.5.0" 549 515 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 804 770 ] 805 771 806 772 [[package]] 807 - name = "num-traits" 808 - version = "0.2.19" 773 + name = "num-conv" 774 + version = "0.1.0" 809 775 source = "registry+https://github.com/rust-lang/crates.io-index" 810 - checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 811 - dependencies = [ 812 - "autocfg", 813 - ] 776 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 814 777 815 778 [[package]] 816 779 name = "object" ··· 919 882 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 920 883 921 884 [[package]] 885 + name = "powerfmt" 886 + version = "0.2.0" 887 + source = "registry+https://github.com/rust-lang/crates.io-index" 888 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 889 + 890 + [[package]] 922 891 name = "proc-macro2" 923 892 version = "1.0.94" 924 893 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 928 897 ] 929 898 930 899 [[package]] 900 + name = "quanta" 901 + version = "0.12.5" 902 + source = "registry+https://github.com/rust-lang/crates.io-index" 903 + checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" 904 + dependencies = [ 905 + "crossbeam-utils", 906 + "libc", 907 + "once_cell", 908 + "raw-cpuid", 909 + "wasi 0.11.0+wasi-snapshot-preview1", 910 + "web-sys", 911 + "winapi", 912 + ] 913 + 914 + [[package]] 931 915 name = "quote" 932 916 version = "1.0.40" 933 917 source = "registry+https://github.com/rust-lang/crates.io-index" 934 918 checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 935 919 dependencies = [ 936 920 "proc-macro2", 921 + ] 922 + 923 + [[package]] 924 + name = "raw-cpuid" 925 + version = "11.5.0" 926 + source = "registry+https://github.com/rust-lang/crates.io-index" 927 + checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" 928 + dependencies = [ 929 + "bitflags", 937 930 ] 938 931 939 932 [[package]] ··· 1239 1232 name = "strong-api-dump" 1240 1233 version = "0.1.0" 1241 1234 dependencies = [ 1242 - "chrono", 1243 1235 "clickhouse", 1244 1236 "dotenv", 1245 1237 "reqwest", 1238 + "serde", 1246 1239 "serde_json", 1247 1240 "strong-api-lib", 1241 + "time", 1248 1242 "tokio", 1243 + "uuid", 1249 1244 ] 1250 1245 1251 1246 [[package]] ··· 1349 1344 ] 1350 1345 1351 1346 [[package]] 1347 + name = "time" 1348 + version = "0.3.40" 1349 + source = "registry+https://github.com/rust-lang/crates.io-index" 1350 + checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" 1351 + dependencies = [ 1352 + "deranged", 1353 + "itoa", 1354 + "num-conv", 1355 + "powerfmt", 1356 + "serde", 1357 + "time-core", 1358 + "time-macros", 1359 + ] 1360 + 1361 + [[package]] 1362 + name = "time-core" 1363 + version = "0.1.4" 1364 + source = "registry+https://github.com/rust-lang/crates.io-index" 1365 + checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 1366 + 1367 + [[package]] 1368 + name = "time-macros" 1369 + version = "0.2.21" 1370 + source = "registry+https://github.com/rust-lang/crates.io-index" 1371 + checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" 1372 + dependencies = [ 1373 + "num-conv", 1374 + "time-core", 1375 + ] 1376 + 1377 + [[package]] 1352 1378 name = "tinystr" 1353 1379 version = "0.7.6" 1354 1380 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1508 1534 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1509 1535 1510 1536 [[package]] 1537 + name = "uuid" 1538 + version = "1.15.1" 1539 + source = "registry+https://github.com/rust-lang/crates.io-index" 1540 + checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" 1541 + dependencies = [ 1542 + "serde", 1543 + ] 1544 + 1545 + [[package]] 1511 1546 name = "vcpkg" 1512 1547 version = "0.2.15" 1513 1548 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1619 1654 ] 1620 1655 1621 1656 [[package]] 1622 - name = "windows-core" 1623 - version = "0.52.0" 1657 + name = "winapi" 1658 + version = "0.3.9" 1624 1659 source = "registry+https://github.com/rust-lang/crates.io-index" 1625 - checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1660 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1626 1661 dependencies = [ 1627 - "windows-targets 0.52.6", 1662 + "winapi-i686-pc-windows-gnu", 1663 + "winapi-x86_64-pc-windows-gnu", 1628 1664 ] 1665 + 1666 + [[package]] 1667 + name = "winapi-i686-pc-windows-gnu" 1668 + version = "0.4.0" 1669 + source = "registry+https://github.com/rust-lang/crates.io-index" 1670 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1671 + 1672 + [[package]] 1673 + name = "winapi-x86_64-pc-windows-gnu" 1674 + version = "0.4.0" 1675 + source = "registry+https://github.com/rust-lang/crates.io-index" 1676 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1629 1677 1630 1678 [[package]] 1631 1679 name = "windows-link"
+1 -1
docker/docker-compose.yml
··· 3 3 image: clickhouse/clickhouse-server:latest 4 4 container_name: clickhouse-server 5 5 volumes: 6 - - ./clickhouse-data:/var/lib/clickhouse 6 + # - ./clickhouse-data:/var/lib/clickhouse 7 7 - ./init.sql:/docker-entrypoint-initdb.d/init.sql 8 8 ports: 9 9 - "8123:8123"
+4 -2
strong-api-dump/Cargo.toml
··· 9 9 reqwest = "0.12.14" 10 10 serde_json = "1.0" 11 11 tokio = { version = "1", features = ["full"] } 12 - clickhouse = "0.13.2" 13 - chrono = "0.4.40" 12 + clickhouse = { version = "0.13.2", features = ["inserter", "time", "uuid"] } 13 + serde = { version = "1.0.219", features = ["derive"] } 14 + uuid = { version = "1.15.1", features = ["serde"] } 15 + time = { version = "0.3.40", features = ["parsing", "macros", "formatting"] } 14 16 15 17 [dev-dependencies] 16 18 clickhouse = { version = "0.13.2", features = ["test-util"] }
+59 -31
strong-api-dump/src/clickhouse_saver.rs
··· 1 1 use clickhouse::Row; 2 - use chrono::NaiveDateTime; 2 + use serde::{Deserialize, Serialize}; 3 3 use std::error::Error; 4 4 use strong_api_lib::data_transformer::Workout; 5 + use time::OffsetDateTime; 6 + use time::format_description::well_known::Rfc3339; 7 + use time::macros::format_description; 8 + use uuid::Uuid; 5 9 6 10 /// This flattened struct represents one set with its workout and exercise context. 7 - #[derive(Row, Debug)] 11 + #[derive(Row, Serialize, Deserialize, Debug)] 8 12 pub struct WorkoutSet { 9 - pub workout_id: String, 13 + #[serde(with = "clickhouse::serde::uuid")] 14 + pub workout_id: Uuid, 10 15 pub workout_name: String, 11 - pub timezone: Option<String>, 12 - pub start_date: Option<NaiveDateTime>, 13 - pub end_date: Option<NaiveDateTime>, 14 - pub exercise_id: String, 16 + pub timezone: String, 17 + #[serde(with = "clickhouse::serde::time::datetime64::millis")] 18 + pub start_date: OffsetDateTime, 19 + #[serde(with = "clickhouse::serde::time::datetime64::millis")] 20 + pub end_date: OffsetDateTime, 21 + #[serde(with = "clickhouse::serde::uuid")] 22 + pub exercise_id: Uuid, 15 23 pub exercise_name: String, 16 - pub set_id: String, 17 - pub weight: Option<f32>, 24 + #[serde(with = "clickhouse::serde::uuid")] 25 + pub set_id: Uuid, 26 + pub weight: f32, 18 27 pub reps: u32, 19 - pub rpe: Option<f32>, 28 + pub rpe: f32, 20 29 } 21 30 22 31 pub struct ClickHouseSaver { ··· 25 34 } 26 35 27 36 impl ClickHouseSaver { 28 - pub fn new(url: &str, username: &str, password: &str, table_name: &str) -> Self { 37 + pub fn new( 38 + url: &str, 39 + username: &str, 40 + password: &str, 41 + database: &str, 42 + table_name: &str, 43 + ) -> Self { 29 44 Self { 30 45 client: clickhouse::Client::default() 31 46 .with_url(url) 32 47 .with_user(username) 33 - .with_password(password), 48 + .with_password(password) 49 + .with_database(database), 34 50 table_name: table_name.to_string(), 35 51 } 36 52 } ··· 45 61 /// 46 62 /// A Result indicating success or any error encountered. 47 63 pub async fn save_workout(&self, workout: &Workout) -> Result<(), Box<dyn Error>> { 48 - let mut rows = Vec::new(); 64 + let mut insert = self.client.insert(&self.table_name)?; 49 65 50 - // Flatten the data from the nested Workout structure. 51 66 for exercise in &workout.exercises { 52 67 for set in &exercise.sets { 53 - let start_dt = workout.start_date.as_ref() 54 - .and_then(|s| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").ok()); 55 - let end_dt = workout.end_date.as_ref() 56 - .and_then(|s| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").ok()); 68 + let start_dt = OffsetDateTime::parse( 69 + &workout.start_date.clone().unwrap_or_default(), 70 + &Rfc3339, 71 + )?; 72 + let end_dt = OffsetDateTime::parse( 73 + &workout.start_date.clone().unwrap_or_default(), 74 + &Rfc3339, 75 + )?; 76 + 77 + dbg!(&start_dt); 57 78 58 - rows.push(WorkoutSet { 59 - workout_id: workout.id.clone(), 79 + let row = WorkoutSet { 80 + workout_id: Uuid::parse_str(&workout.id).expect("workout_id UUID parse failed"), 60 81 workout_name: workout.name.clone(), 61 - timezone: workout.timezone.clone(), 82 + timezone: workout 83 + .timezone 84 + .clone() 85 + .unwrap_or_else(|| "Europe/Berlin".to_string()), 62 86 start_date: start_dt, 63 87 end_date: end_dt, 64 - exercise_id: exercise.id.clone(), 88 + exercise_id: Uuid::parse_str(&exercise.id).expect("exercise UUID parse failed"), 65 89 exercise_name: exercise.name.clone(), 66 - set_id: set.id.clone(), 67 - weight: set.weight, 90 + set_id: Uuid::parse_str(&set.id).expect("set UUID parse failed"), 91 + weight: set.weight.unwrap_or(0.0), 68 92 reps: set.reps, 69 - rpe: set.rpe, 70 - }); 93 + rpe: set.rpe.unwrap_or(0.0), 94 + }; 95 + 96 + insert.write(&row).await?; 71 97 } 72 98 } 73 99 74 - let mut inserter = self.client.insert(&self.table_name).await?; 75 - inserter.write(&rows).await?; 76 - inserter.end().await?; 100 + insert.end().await?; 77 101 78 - println!("Workout data inserted successfully!"); 102 + println!("Workout {} imported successfully", workout.id); 79 103 Ok(()) 80 104 } 81 - } 105 + } 106 + 107 + fn escape_string(s: &str) -> String { 108 + s.to_string() 109 + }
+27 -20
strong-api-dump/src/main.rs
··· 21 21 let start = std::time::Instant::now(); 22 22 23 23 let mut strong_api = StrongApi::new(url); 24 - let mut clickhouse_saver = clickhouse_saver::ClickHouseSaver::new( 25 - env::var("CLICKHOUSE_URL").expect("CLICKHOUSE_URL must be set").as_str(), 26 - env::var("CLICKHOUSE_PASS").expect("CLICKHOUSE_PASS must be set").as_str(), 27 - env::var("CLICKHOUSE_USER").expect("CLICKHOUSE_USER must be set").as_str(), 28 - env::var("CLICKHOUSE_TABLE").expect("CLICKHOUSE_TABLE must be set").as_str(), 24 + let clickhouse_saver = clickhouse_saver::ClickHouseSaver::new( 25 + env::var("CLICKHOUSE_URL") 26 + .expect("CLICKHOUSE_URL must be set") 27 + .as_str(), 28 + env::var("CLICKHOUSE_USER") 29 + .expect("CLICKHOUSE_USER must be set") 30 + .as_str(), 31 + env::var("CLICKHOUSE_PASS") 32 + .expect("CLICKHOUSE_PASS must be set") 33 + .as_str(), 34 + env::var("CLICKHOUSE_DATABASE") 35 + .expect("CLICKHOUSE_USER must be set") 36 + .as_str(), 37 + env::var("CLICKHOUSE_TABLE") 38 + .expect("CLICKHOUSE_TABLE must be set") 39 + .as_str(), 29 40 ); 30 41 31 - strong_api 42 + /*strong_api 32 43 .login(username.as_str(), password.as_str()) 33 44 .await?; 34 - 45 + */ 35 46 let measurements_response; 36 47 // check if measurements.json file exist, if not, fetch the data from the API 37 48 if !std::path::Path::new("measurements.json").exists() { ··· 47 58 measurements_response = serde_json::from_str(&measurements_json)?; 48 59 } 49 60 50 - let user = strong_api.get_user("", 500, vec![Includes::Log]).await?; 61 + //let user = strong_api.get_user("", 500, vec![Includes::Log]).await?; 51 62 52 - //let response_text = std::fs::read_to_string("response.json")?; 53 - //let user: UserResponse = serde_json::from_str(&response_text)?; 63 + let response_text = std::fs::read_to_string("response_1742332448.json")?; 64 + let user: UserResponse = serde_json::from_str(&response_text)?; 54 65 55 66 println!( 56 67 "Measurements count: {}/{}", ··· 66 77 67 78 println!("Workout count: {}", workouts.len()); 68 79 69 - workouts.iter().for_each(|workout| { 70 - println!("Workout: {}", workout.name); 71 - println!("Date: {:?}", workout.start_date); 72 - workout.exercises.iter().for_each(|exercise| { 73 - println!("Name: {}", exercise.name); 74 - exercise.sets.iter().for_each(|set| { 75 - println!("Set: {:?}", set); 76 - }); 77 - }); 78 - }); 80 + for workout in workouts.iter() { 81 + clickhouse_saver 82 + .save_workout(workout) 83 + .await 84 + .expect("Couldn't save workout"); 85 + } 79 86 80 87 let end = start.elapsed(); 81 88