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.

added clickhouse integration

+389 -9
+3 -1
.gitignore
··· 1 1 /target 2 2 .env 3 3 .idea 4 - response.json 4 + response.json 5 + /docker/clickhouse-data 6 + /response.json
+241
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]] 21 36 name = "atomic-waker" 22 37 version = "1.1.2" 23 38 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 57 72 checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 58 73 59 74 [[package]] 75 + name = "bstr" 76 + version = "1.11.3" 77 + source = "registry+https://github.com/rust-lang/crates.io-index" 78 + checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" 79 + dependencies = [ 80 + "memchr", 81 + ] 82 + 83 + [[package]] 60 84 name = "bumpalo" 61 85 version = "3.17.0" 62 86 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 84 108 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 85 109 86 110 [[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 + name = "cityhash-rs" 126 + version = "1.0.1" 127 + source = "registry+https://github.com/rust-lang/crates.io-index" 128 + checksum = "93a719913643003b84bd13022b4b7e703c09342cd03b679c4641c7d2e50dc34d" 129 + 130 + [[package]] 131 + name = "clickhouse" 132 + version = "0.13.2" 133 + source = "registry+https://github.com/rust-lang/crates.io-index" 134 + checksum = "d9894248c4c5a4402f76a56c273836a0c32547ec8a68166aedee7e01b7b8d102" 135 + dependencies = [ 136 + "bstr", 137 + "bytes", 138 + "cityhash-rs", 139 + "clickhouse-derive", 140 + "futures", 141 + "futures-channel", 142 + "http-body-util", 143 + "hyper", 144 + "hyper-util", 145 + "lz4_flex", 146 + "replace_with", 147 + "sealed", 148 + "serde", 149 + "static_assertions", 150 + "thiserror", 151 + "tokio", 152 + "url", 153 + ] 154 + 155 + [[package]] 156 + name = "clickhouse-derive" 157 + version = "0.2.0" 158 + source = "registry+https://github.com/rust-lang/crates.io-index" 159 + checksum = "d70f3e2893f7d3e017eeacdc9a708fbc29a10488e3ebca21f9df6a5d2b616dbb" 160 + dependencies = [ 161 + "proc-macro2", 162 + "quote", 163 + "serde_derive_internals", 164 + "syn", 165 + ] 166 + 167 + [[package]] 87 168 name = "core-foundation" 88 169 version = "0.9.4" 89 170 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 178 259 ] 179 260 180 261 [[package]] 262 + name = "futures" 263 + version = "0.3.31" 264 + source = "registry+https://github.com/rust-lang/crates.io-index" 265 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 266 + dependencies = [ 267 + "futures-channel", 268 + "futures-core", 269 + "futures-executor", 270 + "futures-io", 271 + "futures-sink", 272 + "futures-task", 273 + "futures-util", 274 + ] 275 + 276 + [[package]] 181 277 name = "futures-channel" 182 278 version = "0.3.31" 183 279 source = "registry+https://github.com/rust-lang/crates.io-index" 184 280 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 185 281 dependencies = [ 186 282 "futures-core", 283 + "futures-sink", 187 284 ] 188 285 189 286 [[package]] ··· 193 290 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 194 291 195 292 [[package]] 293 + name = "futures-executor" 294 + version = "0.3.31" 295 + source = "registry+https://github.com/rust-lang/crates.io-index" 296 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 297 + dependencies = [ 298 + "futures-core", 299 + "futures-task", 300 + "futures-util", 301 + ] 302 + 303 + [[package]] 304 + name = "futures-io" 305 + version = "0.3.31" 306 + source = "registry+https://github.com/rust-lang/crates.io-index" 307 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 308 + 309 + [[package]] 310 + name = "futures-macro" 311 + version = "0.3.31" 312 + source = "registry+https://github.com/rust-lang/crates.io-index" 313 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 314 + dependencies = [ 315 + "proc-macro2", 316 + "quote", 317 + "syn", 318 + ] 319 + 320 + [[package]] 196 321 name = "futures-sink" 197 322 version = "0.3.31" 198 323 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 210 335 source = "registry+https://github.com/rust-lang/crates.io-index" 211 336 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 212 337 dependencies = [ 338 + "futures-channel", 213 339 "futures-core", 340 + "futures-io", 341 + "futures-macro", 342 + "futures-sink", 214 343 "futures-task", 344 + "memchr", 215 345 "pin-project-lite", 216 346 "pin-utils", 347 + "slab", 217 348 ] 218 349 219 350 [[package]] ··· 311 442 checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 312 443 313 444 [[package]] 445 + name = "httpdate" 446 + version = "1.0.3" 447 + source = "registry+https://github.com/rust-lang/crates.io-index" 448 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 449 + 450 + [[package]] 314 451 name = "hyper" 315 452 version = "1.6.0" 316 453 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 323 460 "http", 324 461 "http-body", 325 462 "httparse", 463 + "httpdate", 326 464 "itoa", 327 465 "pin-project-lite", 328 466 "smallvec", ··· 383 521 ] 384 522 385 523 [[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]] 386 547 name = "icu_collections" 387 548 version = "1.5.0" 388 549 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 588 749 checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 589 750 590 751 [[package]] 752 + name = "lz4_flex" 753 + version = "0.11.3" 754 + source = "registry+https://github.com/rust-lang/crates.io-index" 755 + checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" 756 + 757 + [[package]] 591 758 name = "memchr" 592 759 version = "2.7.4" 593 760 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 634 801 "security-framework", 635 802 "security-framework-sys", 636 803 "tempfile", 804 + ] 805 + 806 + [[package]] 807 + name = "num-traits" 808 + version = "0.2.19" 809 + source = "registry+https://github.com/rust-lang/crates.io-index" 810 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 811 + dependencies = [ 812 + "autocfg", 637 813 ] 638 814 639 815 [[package]] ··· 770 946 ] 771 947 772 948 [[package]] 949 + name = "replace_with" 950 + version = "0.1.7" 951 + source = "registry+https://github.com/rust-lang/crates.io-index" 952 + checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" 953 + 954 + [[package]] 773 955 name = "reqwest" 774 956 version = "0.12.14" 775 957 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 913 1095 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 914 1096 915 1097 [[package]] 1098 + name = "sealed" 1099 + version = "0.6.0" 1100 + source = "registry+https://github.com/rust-lang/crates.io-index" 1101 + checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" 1102 + dependencies = [ 1103 + "proc-macro2", 1104 + "quote", 1105 + "syn", 1106 + ] 1107 + 1108 + [[package]] 916 1109 name = "security-framework" 917 1110 version = "2.11.1" 918 1111 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 956 1149 ] 957 1150 958 1151 [[package]] 1152 + name = "serde_derive_internals" 1153 + version = "0.29.1" 1154 + source = "registry+https://github.com/rust-lang/crates.io-index" 1155 + checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" 1156 + dependencies = [ 1157 + "proc-macro2", 1158 + "quote", 1159 + "syn", 1160 + ] 1161 + 1162 + [[package]] 959 1163 name = "serde_json" 960 1164 version = "1.0.140" 961 1165 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1026 1230 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1027 1231 1028 1232 [[package]] 1233 + name = "static_assertions" 1234 + version = "1.1.0" 1235 + source = "registry+https://github.com/rust-lang/crates.io-index" 1236 + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1237 + 1238 + [[package]] 1029 1239 name = "strong-api-dump" 1030 1240 version = "0.1.0" 1031 1241 dependencies = [ 1242 + "chrono", 1243 + "clickhouse", 1032 1244 "dotenv", 1033 1245 "reqwest", 1034 1246 "serde_json", ··· 1114 1326 "once_cell", 1115 1327 "rustix", 1116 1328 "windows-sys 0.59.0", 1329 + ] 1330 + 1331 + [[package]] 1332 + name = "thiserror" 1333 + version = "1.0.69" 1334 + source = "registry+https://github.com/rust-lang/crates.io-index" 1335 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1336 + dependencies = [ 1337 + "thiserror-impl", 1338 + ] 1339 + 1340 + [[package]] 1341 + name = "thiserror-impl" 1342 + version = "1.0.69" 1343 + source = "registry+https://github.com/rust-lang/crates.io-index" 1344 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1345 + dependencies = [ 1346 + "proc-macro2", 1347 + "quote", 1348 + "syn", 1117 1349 ] 1118 1350 1119 1351 [[package]] ··· 1384 1616 dependencies = [ 1385 1617 "js-sys", 1386 1618 "wasm-bindgen", 1619 + ] 1620 + 1621 + [[package]] 1622 + name = "windows-core" 1623 + version = "0.52.0" 1624 + source = "registry+https://github.com/rust-lang/crates.io-index" 1625 + checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1626 + dependencies = [ 1627 + "windows-targets 0.52.6", 1387 1628 ] 1388 1629 1389 1630 [[package]]
+5
README.MD
··· 12 12 13 13 STRONG_USER=your_strong_user 14 14 STRONG_PASS=your_strong_pass 15 + 16 + CLICKHOUSE_HOST=clickhouse 17 + CLICKHOUSE_USER=default 18 + CLICKHOUSE_PASS= 19 + CLICKHOUSE_TABLE=strong 15 20 ``` 16 21 3. Run with `cargo run`
+20
docker/docker-compose.yml
··· 1 + services: 2 + clickhouse-server: 3 + image: clickhouse/clickhouse-server:latest 4 + container_name: clickhouse-server 5 + volumes: 6 + - ./clickhouse-data:/var/lib/clickhouse 7 + - ./init.sql:/docker-entrypoint-initdb.d/init.sql 8 + ports: 9 + - "8123:8123" 10 + - "9000:9000" 11 + environment: 12 + - CLICKHOUSE_DB=workouts 13 + - CLICKHOUSE_USER=tolik518 14 + - CLICKHOUSE_PASSWORD=admin 15 + # networks: 16 + # - strong-api-network 17 + # 18 + # networks: 19 + # strong-api-network: 20 + # driver: bridge
+19
docker/init.sql
··· 1 + CREATE DATABASE IF NOT EXISTS workouts; 2 + USE workouts; 3 + 4 + CREATE TABLE workout_sets 5 + ( 6 + `workout_id` String, 7 + `workout_name` String, 8 + `timezone` Nullable(String), 9 + `start_date` Nullable(DateTime), 10 + `end_date` Nullable(DateTime), 11 + `exercise_id` String, 12 + `exercise_name` String, 13 + `set_id` String, 14 + `weight` Nullable(Float32), 15 + `reps` UInt32, 16 + `rpe` Nullable(Float32) 17 + ) 18 + ENGINE = MergeTree 19 + ORDER BY ifNull(start_date, toDateTime('1970-01-01 00:00:00'))
+6 -1
strong-api-dump/Cargo.toml
··· 8 8 dotenv = "0.15" 9 9 reqwest = "0.12.14" 10 10 serde_json = "1.0" 11 - tokio = { version = "1", features = ["full"] } 11 + tokio = { version = "1", features = ["full"] } 12 + clickhouse = "0.13.2" 13 + chrono = "0.4.40" 14 + 15 + [dev-dependencies] 16 + clickhouse = { version = "0.13.2", features = ["test-util"] }
+81
strong-api-dump/src/clickhouse_saver.rs
··· 1 + use clickhouse::Row; 2 + use chrono::NaiveDateTime; 3 + use std::error::Error; 4 + use strong_api_lib::data_transformer::Workout; 5 + 6 + /// This flattened struct represents one set with its workout and exercise context. 7 + #[derive(Row, Debug)] 8 + pub struct WorkoutSet { 9 + pub workout_id: String, 10 + 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, 15 + pub exercise_name: String, 16 + pub set_id: String, 17 + pub weight: Option<f32>, 18 + pub reps: u32, 19 + pub rpe: Option<f32>, 20 + } 21 + 22 + pub struct ClickHouseSaver { 23 + client: clickhouse::Client, 24 + table_name: String, 25 + } 26 + 27 + impl ClickHouseSaver { 28 + pub fn new(url: &str, username: &str, password: &str, table_name: &str) -> Self { 29 + Self { 30 + client: clickhouse::Client::default() 31 + .with_url(url) 32 + .with_user(username) 33 + .with_password(password), 34 + table_name: table_name.to_string(), 35 + } 36 + } 37 + 38 + /// Saves a given workout into ClickHouse by flattening its nested data into rows. 39 + /// 40 + /// # Arguments 41 + /// 42 + /// * `workout` - A reference to the Workout struct. 43 + /// 44 + /// # Returns 45 + /// 46 + /// A Result indicating success or any error encountered. 47 + pub async fn save_workout(&self, workout: &Workout) -> Result<(), Box<dyn Error>> { 48 + let mut rows = Vec::new(); 49 + 50 + // Flatten the data from the nested Workout structure. 51 + for exercise in &workout.exercises { 52 + 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()); 57 + 58 + rows.push(WorkoutSet { 59 + workout_id: workout.id.clone(), 60 + workout_name: workout.name.clone(), 61 + timezone: workout.timezone.clone(), 62 + start_date: start_dt, 63 + end_date: end_dt, 64 + exercise_id: exercise.id.clone(), 65 + exercise_name: exercise.name.clone(), 66 + set_id: set.id.clone(), 67 + weight: set.weight, 68 + reps: set.reps, 69 + rpe: set.rpe, 70 + }); 71 + } 72 + } 73 + 74 + let mut inserter = self.client.insert(&self.table_name).await?; 75 + inserter.write(&rows).await?; 76 + inserter.end().await?; 77 + 78 + println!("Workout data inserted successfully!"); 79 + Ok(()) 80 + } 81 + }
+14 -7
strong-api-dump/src/main.rs
··· 1 + mod clickhouse_saver; 2 + 1 3 use dotenv::dotenv; 2 4 use reqwest::Url; 3 5 use std::env; 4 6 use strong_api_lib::data_transformer::DataTransformer; 5 7 use strong_api_lib::json_response::UserResponse; 6 - use strong_api_lib::strong_api::StrongApi; 8 + use strong_api_lib::strong_api::{Includes, StrongApi}; 7 9 8 10 #[tokio::main] 9 11 async fn main() -> Result<(), Box<dyn std::error::Error>> { ··· 18 20 19 21 let start = std::time::Instant::now(); 20 22 21 - let strong_api = StrongApi::new(url); 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(), 29 + ); 22 30 23 - /* 24 31 strong_api 25 32 .login(username.as_str(), password.as_str()) 26 33 .await?; 27 - */ 28 34 29 35 let measurements_response; 30 36 // check if measurements.json file exist, if not, fetch the data from the API ··· 41 47 measurements_response = serde_json::from_str(&measurements_json)?; 42 48 } 43 49 44 - //let user = strong_api.get_user("", 500, vec![Includes::Log]).await?; 50 + let user = strong_api.get_user("", 500, vec![Includes::Log]).await?; 45 51 46 - let response_text = std::fs::read_to_string("response.json")?; 47 - let user: UserResponse = serde_json::from_str(&response_text)?; 52 + //let response_text = std::fs::read_to_string("response.json")?; 53 + //let user: UserResponse = serde_json::from_str(&response_text)?; 48 54 49 55 println!( 50 56 "Measurements count: {}/{}", ··· 62 68 63 69 workouts.iter().for_each(|workout| { 64 70 println!("Workout: {}", workout.name); 71 + println!("Date: {:?}", workout.start_date); 65 72 workout.exercises.iter().for_each(|exercise| { 66 73 println!("Name: {}", exercise.name); 67 74 exercise.sets.iter().for_each(|set| {