From 5f4785671d61abe9925366ad64fa411d0af14de6 Mon Sep 17 00:00:00 2001 From: arrmlet Date: Fri, 20 Mar 2026 18:40:14 +0200 Subject: [PATCH 1/5] bump version --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d0aaa8f..99dcec0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,7 +232,7 @@ dependencies = [ [[package]] name = "dataverse-cli" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "clap", From d43f79abcd4114a1eaeb2b5d44d09137c5ea3555 Mon Sep 17 00:00:00 2001 From: arrmlet Date: Sun, 22 Mar 2026 15:47:19 +0200 Subject: [PATCH 2/5] Migrate from HTTP/JSON transcoding to native gRPC via tonic The Macrocosmos API uses gRPC-Web JSON transcoding over HTTP/2, which goes through an unstable ALB layer that frequently returns 500 errors and timeouts. The Python SDK uses native gRPC and is unaffected. This migration replaces reqwest HTTP calls with tonic native gRPC: - Add vendored proto files for sn13.v1 and gravity.v1 - Add build.rs for tonic-build proto compilation - Rewrite ApiClient to use tonic channels with auth interceptor - Convert prost_types::Struct responses to serde_json::Value - Map gRPC status codes to user-friendly error messages - All command/display/config layers unchanged Tested: search x, search reddit, gravity status, dry-run all work. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 986 +++++++++++++---------------- Cargo.toml | 9 +- build.rs | 12 + proto/gravity/v1/gravity.proto | 912 ++++++++++++++++++++++++++ proto/sn13/v1/sn13_validator.proto | 89 +++ src/api/client.rs | 719 ++++++++++++++++++--- 6 files changed, 2067 insertions(+), 660 deletions(-) create mode 100644 build.rs create mode 100644 proto/gravity/v1/gravity.proto create mode 100644 proto/sn13/v1/sn13_validator.proto diff --git a/Cargo.lock b/Cargo.lock index 99dcec0..0931e49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "ansi-str" version = "0.8.0" @@ -83,12 +92,98 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" @@ -101,12 +196,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - [[package]] name = "bytecount" version = "0.6.9" @@ -135,12 +224,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "clap" version = "4.6.0" @@ -209,6 +292,22 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "csv" version = "1.4.0" @@ -240,12 +339,15 @@ dependencies = [ "csv", "dialoguer", "dirs", - "reqwest", + "prost", + "prost-types", "serde", "serde_json", "tabled", "tokio", "toml", + "tonic", + "tonic-build", ] [[package]] @@ -283,15 +385,10 @@ dependencies = [ ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" @@ -327,6 +424,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "fnv" version = "1.0.7" @@ -339,15 +442,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -394,24 +488,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", ] [[package]] @@ -422,7 +500,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi 6.0.0", + "r-efi", "wasip2", "wasip3", ] @@ -439,13 +517,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.5" @@ -512,6 +596,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -526,6 +616,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -535,20 +626,16 @@ dependencies = [ ] [[package]] -name = "hyper-rustls" -version = "0.27.7" +name = "hyper-timeout" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "http", "hyper", "hyper-util", - "rustls", - "rustls-pki-types", + "pin-project-lite", "tokio", - "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -557,104 +644,20 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", - "ipnet", "libc", - "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.3", "tokio", "tower-service", "tracing", ] -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "id-arena" version = "2.3.0" @@ -662,24 +665,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" +name = "indexmap" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "icu_normalizer", - "icu_properties", + "autocfg", + "hashbrown 0.12.3", ] [[package]] @@ -695,43 +687,26 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.12.0" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "iri-string" -version = "0.7.10" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ - "memchr", - "serde", + "either", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "js-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -759,12 +734,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - [[package]] name = "log" version = "0.4.29" @@ -772,10 +741,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "lru-slab" -version = "0.1.2" +name = "matchit" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" @@ -783,6 +752,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -800,6 +775,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "nom" version = "7.1.3" @@ -822,6 +803,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -847,6 +834,36 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.13.0", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -859,15 +876,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -919,58 +927,55 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.11.9" +name = "prost" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", + "prost-derive", ] [[package]] -name = "quinn-proto" -version = "0.11.14" +name = "prost-build" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", + "heck 0.5.0", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.117", + "tempfile", ] [[package]] -name = "quinn-udp" -version = "0.5.14" +name = "prost-derive" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", ] [[package]] @@ -982,12 +987,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -996,19 +995,20 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.9.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -1016,11 +1016,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.2.17", ] [[package]] @@ -1035,44 +1035,34 @@ dependencies = [ ] [[package]] -name = "reqwest" -version = "0.12.28" +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "base64", - "bytes", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", ] +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "ring" version = "0.17.14" @@ -1087,12 +1077,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustix" version = "1.1.4" @@ -1112,6 +1096,7 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -1120,13 +1105,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "web-time", "zeroize", ] @@ -1153,6 +1158,38 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -1211,18 +1248,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "shell-words" version = "1.1.1" @@ -1249,19 +1274,23 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.3" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "socket2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] [[package]] name = "strsim" @@ -1302,20 +1331,6 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "tabled" @@ -1395,31 +1410,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.50.0" @@ -1430,7 +1420,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -1456,6 +1446,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -1496,7 +1497,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime", @@ -1510,35 +1511,83 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "rustls-native-certs", + "rustls-pemfile", + "socket2 0.5.10", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.117", +] + [[package]] name = "tower" -version = "0.5.3" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "indexmap 1.9.3", + "pin-project", "pin-project-lite", - "sync_wrapper", + "rand", + "slab", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] -name = "tower-http" -version = "0.6.8" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ - "bitflags", - "bytes", + "futures-core", "futures-util", - "http", - "http-body", - "iri-string", "pin-project-lite", - "tower", + "sync_wrapper", "tower-layer", "tower-service", ] @@ -1562,9 +1611,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1604,24 +1665,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -1682,65 +1725,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" -dependencies = [ - "unicode-ident", -] - [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1758,7 +1742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.13.0", "wasm-encoder", "wasmparser", ] @@ -1771,39 +1755,10 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.13.0", "semver", ] -[[package]] -name = "web-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -1938,7 +1893,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap", + "indexmap 2.13.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -1969,7 +1924,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -1988,7 +1943,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.13.0", "log", "semver", "serde", @@ -1998,35 +1953,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.42" @@ -2047,66 +1973,12 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 9fc321c..4b7b0ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT" repository = "https://github.com/macrocosm-os/dataverse-cli" keywords = ["bittensor", "macrocosmos", "social-data", "cli", "sn13"] categories = ["command-line-utilities"] -include = ["src/**/*", "Cargo.toml", "LICENSE", "README.md", "AGENTS.md"] +include = ["src/**/*", "proto/**/*", "build.rs", "Cargo.toml", "LICENSE", "README.md", "AGENTS.md"] [[bin]] name = "dv" @@ -15,7 +15,6 @@ path = "src/main.rs" [dependencies] clap = { version = "4", features = ["derive", "env"] } -reqwest = { version = "0.12", features = ["json", "rustls-tls", "http2"], default-features = false } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } @@ -26,6 +25,12 @@ dirs = "6" toml = "0.8" csv = "1" dialoguer = "0.11" +tonic = { version = "0.12", features = ["tls", "tls-native-roots", "transport"] } +prost = "0.13" +prost-types = "0.13" + +[build-dependencies] +tonic-build = "0.12" [profile.release] opt-level = 3 diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..06ffd82 --- /dev/null +++ b/build.rs @@ -0,0 +1,12 @@ +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_server(false) + .compile_protos( + &[ + "proto/sn13/v1/sn13_validator.proto", + "proto/gravity/v1/gravity.proto", + ], + &["proto"], + )?; + Ok(()) +} diff --git a/proto/gravity/v1/gravity.proto b/proto/gravity/v1/gravity.proto new file mode 100644 index 0000000..f032b99 --- /dev/null +++ b/proto/gravity/v1/gravity.proto @@ -0,0 +1,912 @@ +syntax = "proto3"; + +package gravity.v1; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +option go_package = "macrocosm-os/rift/constellation_api/gen/gravity/v1"; + +service GravityService { + // Lists all data collection tasks for a user + rpc GetPopularTags(google.protobuf.Empty) returns (GetPopularTagsResponse); + + // Lists all data collection tasks for a user + rpc GetGravityTasks(GetGravityTasksRequest) returns (GetGravityTasksResponse); + + // Get all marketplace crawlers + rpc GetMarketplaceCrawlers(google.protobuf.Empty) returns (GetMarketplaceCrawlersResponse); + + // Gets raw miner files for a specific crawler + rpc GetCrawlerRawMinerFiles(GetCrawlerRequest) returns (CrawlerRawMinerFilesResponse); + + // Get the parent workflow id (the id of the ui workflow) for this crawler + rpc GetCrawlerParentTaskId(GetCrawlerRequest) returns (CreateGravityTaskResponse); + + // Get a single crawler by its ID + rpc GetCrawler(GetCrawlerRequest) returns (GetCrawlerResponse); + + // Upsert marketplace task metadata + rpc UpsertMarketplaceTaskMetadata(UpsertMarketplaceTaskMetadataRequest) returns (UpsertResponse); + + // Upsert marketplace task suggestions + rpc UpsertMarketplaceTaskSuggestions(UpsertMarketplaceTaskSuggestionsRequest) returns (UpsertResponse); + + // Get marketplace task suggestions + rpc GetMarketplaceTaskSuggestions(GetMarketplaceTaskSuggestionsRequest) returns (GetMarketplaceDatasetsResponse); + + // Create a new gravity task + rpc CreateGravityTask(CreateGravityTaskRequest) + returns (CreateGravityTaskResponse); + + // Gets all dataset files for a given marketplace gravity task (no user_id check, validates against marketplace tasks table) + rpc GetGravityMarketplaceTaskDatasetFiles(GetGravityTaskDatasetFilesRequest) + returns (GetGravityTaskDatasetFilesResponse); + + // Build a dataset for a single crawler + rpc BuildDataset(BuildDatasetRequest) returns (BuildDatasetResponse); + + // Get the dataset build status and results + rpc GetDataset(GetDatasetRequest) returns (GetDatasetResponse); + + // Cancel a gravity task and any crawlers associated with it + rpc CancelGravityTask(CancelGravityTaskRequest) + returns (CancelGravityTaskResponse); + + // Cancel dataset build if it is in progress and purges the dataset + rpc CancelDataset(CancelDatasetRequest) returns (CancelDatasetResponse); + + // Refund user if fewer rows are returned + rpc DatasetBillingCorrection(DatasetBillingCorrectionRequest) + returns (DatasetBillingCorrectionResponse); + + // Gets the available datsets for use in Dataset Marketplace + rpc GetMarketplaceDatasets(GetMarketplaceDatasetsRequest) + returns (GetMarketplaceDatasetsResponse); + + // Gets all dataset files for a given gravity task + rpc GetGravityTaskDatasetFiles(GetGravityTaskDatasetFilesRequest) + returns (GetGravityTaskDatasetFilesResponse); + + // Publishes a dataset into the Marketplace + rpc PublishDataset(PublishDatasetRequest) returns (UpsertResponse); + + // Get crawler data for DD submission + rpc GetActiveUserTasks(google.protobuf.Empty) + returns (GetActiveUserTasksResponse); + + // Get crawler data for DD submission for the marketplace user + rpc GetMarketplaceCrawlerDataForDDSubmission(GetMarketplaceCrawlerDataForDDSubmissionRequest) + returns (GetMarketplaceCrawlerDataForDDSubmissionResponse); + + // Upserts a crawler into the Gravity state DB + rpc UpsertCrawler(UpsertCrawlerRequest) returns (UpsertResponse); + + // Upserts a crawler criteria into the Gravity state DB + rpc InsertCrawlerCriteria(InsertCrawlerCriteriaRequest) + returns (UpsertResponse); + + // Upserts a gravity task into the Gravity state DB + rpc UpsertGravityTask(UpsertGravityTaskRequest) + returns (UpsertGravityTaskResponse); + // Upserts a dataset into to the Gravity state DB + rpc UpsertDataset(UpsertDatasetRequest) returns (UpsertResponse); + + // Inserts a dataset file row into the Gravity state DB + rpc InsertDatasetFile(InsertDatasetFileRequest) returns (UpsertResponse); + + // Upserts a nebula into the Gravity nebula DB + rpc UpsertNebula(UpsertNebulaRequest) returns (UpsertResponse); + + // Builds all datasets for a task (additionally cancels crawlers with no data) + rpc BuildAllDatasets(BuildAllDatasetsRequest) + returns (BuildAllDatasetsResponse); + + // Builds datasets for multiple crawlers within a single gravity task periodically + rpc BuildUserDatasetsPeriodically(BuildAllDatasetsRequest) + returns (BuildAllDatasetsResponse); + + // Charges a user for dataset rows + rpc ChargeForDatasetRows(ChargeForDatasetRowsRequest) + returns (UpsertResponse); + + // Gets crawler history for a gravity task + rpc GetCrawlerHistory(GetCrawlerHistoryRequest) + returns (GetCrawlerHistoryResponse); + + // Completes a crawler + rpc CompleteCrawler(CompleteCrawlerRequest) returns (UpsertResponse); + + // Upserts raw miner files (parquet paths) for a crawler + rpc UpsertRawMinerFiles(UpsertRawMinerFilesRequest) returns (UpsertResponse); + + // Upserts raw miner files (parquet paths) for a crawler + rpc UpsertHotkeys(UpsertHotkeysRequest) returns (UpsertResponse); + + // Gets all hotkeys from the Gravity state DB + rpc GetHotkeys(google.protobuf.Empty) returns (GetHotkeysResponse); + + // Purchase a marketplace dataset + rpc BuyMarketplaceDataset(BuyMarketplaceDatasetRequest) + returns (BuyMarketplaceDatasetResponse); + + // Get all marketplace datasets owned by the authenticated user + rpc GetUserMarketplaceDatasets(google.protobuf.Empty) + returns (GetUserMarketplaceDatasetsResponse); + + // Upserts pre-built user dataset records + rpc UpsertPreBuiltUserDatasets(UpsertPreBuiltUserDatasetsRequest) returns (UpsertResponse); + + // Gets pre-built user dataset records for a gravity task + rpc GetPreBuiltUserDatasets(GetPreBuiltUserDatasetsRequest) returns (GetPreBuiltUserDatasetsResponse); +} + +// UpsertRawMinerFilesRequest is the request message for UpsertRawMinerFiles +message UpsertRawMinerFilesRequest { + // crawler_id: the ID of the crawler + string crawler_id = 1; + // parquet_paths: the paths to the raw miner files collected + repeated string parquet_paths = 2; + // path_sizes: the sizes of the raw miner files collected + repeated int64 path_sizes = 3; +} +// GetHotkeysResponse is the response message for getting hotkeys +message GetHotkeysResponse { + // hotkeys: the hotkeys + repeated string hotkeys = 1; +} + +// BuyMarketplaceDatasetRequest is the request to purchase a dataset +message BuyMarketplaceDatasetRequest { + // gravity_task_id: the marketplace dataset's gravity task id to purchase + string gravity_task_id = 1; +} + +// BuyMarketplaceDatasetResponse is the response to a dataset purchase +message BuyMarketplaceDatasetResponse { + // success: whether the purchase succeeded + bool success = 1; + // message: optional detail + string message = 2; + // purchase_transaction_id: billing transaction id + string purchase_transaction_id = 3; +} + +// UserMarketplaceDataset represents a single owned dataset record +message UserMarketplaceDataset { + string gravity_task_id = 1; + google.protobuf.Timestamp created_at = 2; + int64 purchase_price_cents = 3; + string purchase_transaction_id = 4; +} + +// GetUserMarketplaceDatasetsResponse lists owned datasets +message GetUserMarketplaceDatasetsResponse { + repeated UserMarketplaceDataset user_datasets = 1; +} + +// UpsertHotkeysRequest is the request message for upserting hotkeys +message UpsertHotkeysRequest { + // hotkeys: the hotkeys to upsert + repeated string hotkeys = 1; +} + +// UpsertMarketplaceTaskSuggestionsRequest is the request message for upserting marketplace task suggestions +message UpsertMarketplaceTaskSuggestionsRequest { + // gravity_task_id: the id of the gravity task + string gravity_task_id = 1; + // suggested_gravity_task_ids: the ids of the suggested gravity tasks + repeated string suggested_gravity_task_ids = 2; +} + +// GetMarketplaceTaskSuggestionsRequest is the request message for getting marketplace task suggestions +message GetMarketplaceTaskSuggestionsRequest { + // gravity_task_id: the id of the gravity task + string gravity_task_id = 1; +} + +// GetMarketplaceTaskSuggestionsResponse is the response message for getting marketplace task suggestions +message GetMarketplaceTaskSuggestionsResponse { + // suggested_gravity_task_ids: the ids of the suggested gravity tasks + repeated string suggested_gravity_task_ids = 1; +} + +// PopularTag is a single popular tag along with its count +message PopularTag { + // tag: the popular tag + string tag = 1; + // count: the count of the tag + uint64 count = 2; +} + + +// GetPopularTagsResponse is the response message for getting popular tags +message GetPopularTagsResponse { + // popular_tags: the popular tags + repeated PopularTag popular_tags = 1; +} + +// PublishDatasetRequest is the request message for publishing a dataset +message PublishDatasetRequest { + // dataset_id: the ID of the dataset + string dataset_id = 1; +} + +// UpsertMarketplaceTaskMetadataRequest +message UpsertMarketplaceTaskMetadataRequest { + // gravity_task_id: the id of the gravity task + string gravity_task_id = 1; + // description: a description of the curated gravity task + string description = 2; + // name: the name of the curated task + string name = 3; + // image_url: points to an image related to the task + string image_url = 4; + // tags: a set of tags for this task + repeated string tags = 5; +} +// GetMarketplaceDatasetsRequest is the request message for getting marketplace datasets +message GetMarketplaceDatasetsRequest { + // popular: whether to return popular datasets + bool popular = 1; +} + +// Crawler is a single crawler workflow that registers a single job +// (platform/topic) on SN13's dynamic desirability engine +message Crawler { + // crawler_id: the ID of the crawler + string crawler_id = 1; + // criteria: the contents of the job and the notification details + CrawlerCriteria criteria = 2; + // start_time: the time the crawler was created + google.protobuf.Timestamp start_time = 3; + // deregistration_time: the time the crawler was deregistered + google.protobuf.Timestamp deregistration_time = 4; + // archive_time: the time the crawler was archived + google.protobuf.Timestamp archive_time = 5; + // state: the current state of the crawler + CrawlerState state = 6; + // dataset_workflows: the IDs of the dataset workflows that are associated + // with the crawler + repeated string dataset_workflows = 7; + // parquet_paths: the paths to the raw miner files collected + repeated string parquet_paths = 8; +} + +// UpsertCrawlerRequest for upserting a crawler and its criteria +message UpsertCrawlerRequest { + // gravity_task_id: the parent workflow id -- in this case the multicrawler id + string gravity_task_id = 1; + // crawler: the crawler to upsert into the database + Crawler crawler = 2; +} + +// UpsertResponse is the response message for upserting a crawler +message UpsertResponse { + // message: the message of upserting a crawler (currently hardcoded to + // "success") + string message = 1; +} + +// UpsertGravityTaskRequest for upserting a gravity task +message UpsertGravityTaskRequest { + // gravity_task: the gravity task to upsert into the database + GravityTaskRequest gravity_task = 1; +} + +// UpsertGravityTaskResponse is the response message for upserting a gravity +// task +message UpsertGravityTaskResponse { + // message: the message of upserting a gravity task (currently hardcoded to + // "success") + string message = 1; +} + +// GravityTaskRequest represents the data needed to upsert a gravity task +message GravityTaskRequest { + // id: the ID of the gravity task + string id = 1; + // name: the name of the gravity task + string name = 2; + // status: the status of the gravity task + string status = 3; + // start_time: the start time of the gravity task + google.protobuf.Timestamp start_time = 4; + // notification_to: the notification email address + string notification_to = 5; + // notification_link: the notification redirect link + string notification_link = 6; +} + +// UpsertCrawlerCriteriaRequest for upserting a crawler and its criteria +message InsertCrawlerCriteriaRequest { + // crawler_id: the id of the crawler + string crawler_id = 1; + // crawler_criteria: the crawler criteria to upsert into the database + CrawlerCriteria crawler_criteria = 2; +} + +// CrawlerCriteria is the contents of the job and the notification details +message CrawlerCriteria { + // platform: the platform of the job ('x' or 'reddit') + string platform = 1; + // topic: the topic of the job (e.g. '#ai' for X, 'r/ai' for Reddit) + optional string topic = 2; + // notification: the details of the notification to be sent to the user + CrawlerNotification notification = 3; + // mock: Used for testing purposes (optional, defaults to false) + bool mock = 4; + // user_id: the ID of the user who created the gravity task + string user_id = 5; + // keyword: the keyword to search for in the job (optional) + optional string keyword = 6; + // post_start_datetime: the start date of the job (optional) + optional google.protobuf.Timestamp post_start_datetime = 7; + // post_end_datetime: the end date of the job (optional) + optional google.protobuf.Timestamp post_end_datetime = 8; +} + +// CrawlerNotification is the details of the notification to be sent to the user +message CrawlerNotification { + // to: the email address of the user + string to = 1; + // link: the redirect link in the email where the user can view the dataset + string link = 2; +} + +// HfRepo is a single Hugging Face repository that contains data for a crawler +message HfRepo { + // repo_name: the name of the Hugging Face repository + string repo_name = 1; + // row_count: the number of rows in the repository for the crawler criteria + uint64 row_count = 2; + // last_update: the last recorded time the repository was updated + string last_update = 3; +} + +// CrawlerState is the current state of the crawler +message CrawlerState { + // status: the current status of the crawler + // "Pending" -- Crawler is pending submission to the SN13 Validator + // "Submitted" -- Crawler is submitted to the SN13 Validator + // "Running" -- Crawler is running (we got the first update) + // "Completed" -- Crawler is completed (timer expired) + // "Cancelled" -- Crawler is cancelled by user via cancellation of workflow + // "Archived" -- Crawler is archived (now read-only i.e. no new dataset) + // "Failed" -- Crawler failed to run + string status = 1; + // bytes_collected: the estimated number of bytes collected by the crawler + uint64 bytes_collected = 2; + // records_collected: the estimated number of records collected by the crawler + uint64 records_collected = 3; + // repos: the Hugging Face repositories that contain data for a crawler + repeated HfRepo repos = 4; +} + +// GravityTaskState is the current state of a gravity task +message GravityTaskState { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; + // name: the name given by the user of the gravity task + string name = 2; + // status: the current status of the gravity task + string status = 3; + // start_time: the time the gravity task was created + google.protobuf.Timestamp start_time = 4; + // crawler_ids: the IDs of the crawler workflows that are associated with the + // gravity task + repeated string crawler_ids = 5; + // crawler_workflows: the crawler workflows that are associated with the + // gravity task + repeated Crawler crawler_workflows = 6; +} + +// GravityMarketplaceTaskState is the current state of a gravity task for marketplace display +message GravityMarketplaceTaskState { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; + // name: the name given by the user of the gravity task + string name = 2; + // status: the current status of the gravity task + string status = 3; + // start_time: the time the gravity task was created + google.protobuf.Timestamp start_time = 4; + // crawler_ids: the IDs of the crawler workflows that are associated with the + // gravity task + repeated string crawler_ids = 5; + // crawler_workflows: the crawler workflows that are associated with the + // gravity task + repeated Crawler crawler_workflows = 6; + // task_records_collected: the total number of records collected across all crawlers for this task + uint64 task_records_collected = 7; + // task_bytes_collected: the total number of bytes collected across all crawlers for this task + uint64 task_bytes_collected = 8; + // description: description from gravity_marketplace_task_metadata + string description = 9; + // image_url: image url from gravity_marketplace_task_metadata + string image_url = 10; + // view_count: number of views from gravity_marketplace_task_download_history + uint64 view_count = 11; + // download_count: number of downloads from gravity_marketplace_task_download_history + uint64 download_count = 12; + // tags: set of tags from gravity_marketplace_task_tags (accumulated) + repeated string tags = 13; +} + +// GetGravityTasksRequest is the request message for listing gravity tasks for a +// user +message GetGravityTasksRequest { + // gravity_task_id: the ID of the gravity task (optional, if not provided, all + // gravity tasks for the user will be returned) + optional string gravity_task_id = 1; + // include_crawlers: whether to include the crawler states in the response + optional bool include_crawlers = 2; +} + +// GetGravityTasksResponse is the response message for listing gravity tasks for +// a user +message GetGravityTasksResponse { + // gravity_task_states: the current states of the gravity tasks + repeated GravityTaskState gravity_task_states = 1; +} + +// GravityTask defines a crawler's criteria for a single job (platform/topic) +message GravityTask { + // topic: the topic of the job (e.g. '#ai' for X, 'r/ai' for Reddit) + optional string topic = 1; + // platform: the platform of the job ('x' or 'reddit') + string platform = 2; + // keyword: the keyword to search for in the job (optional) + optional string keyword = 3; + // post_start_datetime: the start date of the job (optional) + optional google.protobuf.Timestamp post_start_datetime = 4; + // post_end_datetime: the end date of the job (optional) + optional google.protobuf.Timestamp post_end_datetime = 5; +} + +// NotificationRequest is the request message for sending a notification to a +// user when a dataset is ready to download +message NotificationRequest { + // type: the type of notification to send ('email' is only supported + // currently) + string type = 1; + // address: the address to send the notification to (only email addresses are + // supported currently) + string address = 2; + // redirect_url: the URL to include in the notication message that redirects + // the user to any built datasets + optional string redirect_url = 3; +} + +// GetCrawlerRequest is the request message for getting a crawler +message GetCrawlerRequest { + // crawler_id: the ID of the crawler + string crawler_id = 1; +} + +// GetMarketplaceCrawlersResponse is the response message holding all marketplace crawlers +message GetMarketplaceCrawlersResponse { + // crawler_id: the ID of the crawler + repeated string crawler_id = 1; +} +// CompleteCrawlerRequest is the request message for cancelling a crawler +message CompleteCrawlerRequest { + // crawler_id: the ID of the crawler + string crawler_id = 1; + // status: ending status of the crawler + string status = 3; + // removed field + reserved 2; + reserved "gravity_task_id"; +} + +// GetCrawlerResponse is the response message for getting a crawler +message GetCrawlerResponse { + // crawler: the crawler + Crawler crawler = 1; +} + +// CreateGravityTaskRequest is the request message for creating a new gravity +// task +message CreateGravityTaskRequest { + // gravity_tasks: the criteria for the crawlers that will be created + repeated GravityTask gravity_tasks = 1; + // name: the name of the gravity task (optional, default will generate a + // random name) + string name = 2; + // notification_requests: the details of the notification to be sent to the + // user when a dataset + // that is automatically generated upon completion of the crawler is ready + // to download (optional) + repeated NotificationRequest notification_requests = 3; + // gravity_task_id: the ID of the gravity task (optional, default will + // generate a random ID) + optional string gravity_task_id = 4; +} + +// CreateGravityTaskResponse is the response message for creating a new gravity +// task +message CreateGravityTaskResponse { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; +} + +// BuildDatasetRequest is the request message for manually requesting the +// building of a dataset for a single crawler +message BuildDatasetRequest { + // crawler_id: the ID of the crawler that will be used to build the dataset + string crawler_id = 1; + // notification_requests: the details of the notification to be sent to the + // user when the dataset is ready to download (optional) + repeated NotificationRequest notification_requests = 2; + // max_rows: the maximum number of rows to include in the dataset (optional, + // defaults to 500) + int64 max_rows = 3; + // is_periodic: determines whether the datasets to build are for periodic build + optional bool is_periodic = 4; +} + +// BuildDatasetResponse is the response message for manually requesting the +// building of a dataset for a single crawler +// - dataset: the dataset that was built +message BuildDatasetResponse { + // dataset_id: the ID of the dataset + string dataset_id = 1; + // dataset: the dataset that was built + Dataset dataset = 2; +} + +// BuildAllDatasetsRequest is the request message for building all datasets +// belonging to a workflow +message BuildAllDatasetsRequest { + // gravityTaskId specifies which task to build + string gravity_task_id = 1; + // specifies how much of each crawler to build for workflow + repeated BuildDatasetRequest build_crawlers_config = 2; +} + +message BuildAllDatasetsResponse { + string gravity_task_id = 1; + repeated Dataset datasets = 2; +} + +// ChargeForDatasetRowsRequest is the request message for charging a user for dataset rows +message ChargeForDatasetRowsRequest { + // crawler_id: the ID of the crawler that was used to build the dataset + string crawler_id = 1; + // row_count: the number of rows to charge for + int64 row_count = 2; +} + +message Nebula { + // error: nebula build error message + string error = 1; + // file_size_bytes: the size of the file in bytes + int64 file_size_bytes = 2; + // url: the URL of the file + string url = 3; +} + +// Dataset contains the progress and results of a dataset build +message Dataset { + // crawler_workflow_id: the ID of the parent crawler for this dataset + string crawler_workflow_id = 1; + // create_date: the date the dataset was created + google.protobuf.Timestamp create_date = 2; + // expire_date: the date the dataset will expire (be deleted) + google.protobuf.Timestamp expire_date = 3; + // files: the details about the dataset files that are included in the dataset + repeated DatasetFile files = 4; + // status: the status of the dataset + string status = 5; + // status_message: the message of the status of the dataset + string status_message = 6; + // steps: the progress of the dataset build + repeated DatasetStep steps = 7; + // total_steps: the total number of steps in the dataset build + int64 total_steps = 8; + // nebula: the details about the nebula that was built + Nebula nebula = 9; +} + +// UpsertDatasetRequest contains the dataset id to insert and the dataset +// details +message UpsertDatasetRequest { + // dataset_id: a unique id for the dataset + string dataset_id = 1; + // dataset: the details of the dataset + Dataset dataset = 2; +} + +// UpsertNebulaRequest contains the dataset id and nebula details to upsert +message UpsertNebulaRequest { + // dataset_id: a unique id for the dataset + string dataset_id = 1; + // nebula_id: a unique id for the nebula + string nebula_id = 2; + // nebula: the details of the nebula + Nebula nebula = 3; +} + +// InsertDatasetFileRequest contains the dataset id to insert into and the +// dataset file details +message InsertDatasetFileRequest { + // dataset_id: the ID of the dataset to attach the file to + string dataset_id = 1; + // files: the dataset files to insert + repeated DatasetFile files = 2; +} + +// DatasetFile contains the details about a dataset file +message DatasetFile { + // file_name: the name of the file + string file_name = 1; + // file_size_bytes: the size of the file in bytes + uint64 file_size_bytes = 2; + // last_modified: the date the file was last modified + google.protobuf.Timestamp last_modified = 3; + // num_rows: the number of rows in the file + uint64 num_rows = 4; + // s3_key: the key of the file in S3 (internal use only) + string s3_key = 5; + // url: the URL of the file (public use) + string url = 6; +} + +// DatasetStep contains one step of the progress of a dataset build +// (NOTE: each step varies in time and complexity) +message DatasetStep { + // progress: the progress of this step in the dataset build (0.0 - 1.0) + double progress = 1; + // step: the step number of the dataset build (1-indexed) + int64 step = 2; + // step_name: description of what is happening in the step + string step_name = 3; +} + +// GetDatasetRequest is the request message for getting the status of a dataset +message GetDatasetRequest { + // dataset_id: the ID of the dataset + string dataset_id = 1; +} + +// GetDatasetResponse is the response message for getting the status of a +// dataset +message GetDatasetResponse { + // dataset: the dataset that is being built + Dataset dataset = 1; +} + +// CancelGravityTaskRequest is the request message for cancelling a gravity task +message CancelGravityTaskRequest { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; +} + +// CancelGravityTaskResponse is the response message for cancelling a gravity +// task +message CancelGravityTaskResponse { + // message: the message of the cancellation of the gravity task (currently + // hardcoded to "success") + string message = 1; +} + +// CancelDatasetRequest is the request message for cancelling a dataset build +message CancelDatasetRequest { + // dataset_id: the ID of the dataset + string dataset_id = 1; +} + +// CancelDatasetResponse is the response message for cancelling a dataset build +message CancelDatasetResponse { + // message: the message of the cancellation of the dataset build (currently + // hardcoded to "success") + string message = 1; +} + +// DatasetBillingCorrectionRequest is the request message for refunding a user +message DatasetBillingCorrectionRequest { + // requested_row_count: number of rows expected by the user + int64 requested_row_count = 1; + // actual_row_count: number of rows returned by gravity + int64 actual_row_count = 2; +} + +// DatasetBillingCorrectionResponse is the response message for refunding a user +message DatasetBillingCorrectionResponse { + // refund_amount + double refund_amount = 1; +} + +// GetMarketplaceDatasetsResponse returns the dataset metadata to be used in +// Marketplace +message GetMarketplaceDatasetsResponse { + // datasets: list of marketplace datasets + repeated GravityMarketplaceTaskState datasets = 1; +} + +// GetGravityTaskDatasetFilesRequest is the request message for getting dataset +// files for a gravity task +message GetGravityTaskDatasetFilesRequest { + // gravity_task_id: the ID of the gravity task (required) + string gravity_task_id = 1; +} + +// CrawlerDatasetFiles contains dataset files for a specific crawler +message CrawlerDatasetFiles { + // crawler_id: the ID of the crawler + string crawler_id = 1; + // dataset_files: the dataset files associated with this crawler + repeated DatasetFileWithId dataset_files = 2; +} + +// CrawlerRawMinerFiles contains raw miner files for a specific crawler +message CrawlerRawMinerFilesResponse { + // crawler_id: the ID of the crawler + string crawler_id = 1; + // s3_paths: the S3 paths associated with this crawler + repeated string s3_paths = 2; + // file_size_bytes: the sizes of the raw miner files collected + repeated int64 file_size_bytes = 3; +} + +// DatasetFileWithId extends DatasetFile to include the dataset ID +message DatasetFileWithId { + // dataset_id: the ID of the dataset this file belongs to + string dataset_id = 1; + // file_name: the name of the file + string file_name = 2; + // file_size_bytes: the size of the file in bytes + uint64 file_size_bytes = 3; + // last_modified: the date the file was last modified + google.protobuf.Timestamp last_modified = 4; + // num_rows: the number of rows in the file + uint64 num_rows = 5; + // s3_key: the key of the file in S3 (internal use only) + string s3_key = 6; + // url: the URL of the file (public use) + string url = 7; + // nebula_url: the url of a nebula + string nebula_url = 8; +} + +// GetGravityTaskDatasetFilesResponse is the response message for getting +// dataset files for a gravity task +message GetGravityTaskDatasetFilesResponse { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; + // crawler_dataset_files: dataset files grouped by crawler + repeated CrawlerDatasetFiles crawler_dataset_files = 2; +} + +// GetCrawlerHistoryRequest is the request message for getting crawler history +// associated to the provided gravity_task_id +message GetCrawlerHistoryRequest { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; +} + +// CrawlerHistoryEntry represents a single history entry for a crawler +message CrawlerHistoryEntry { + // ingest_dt: the timestamp when this entry was ingested + google.protobuf.Timestamp ingest_dt = 1; + // records_collected: the number of records collected + int64 records_collected = 2; + // bytes_collected: the number of bytes collected + int64 bytes_collected = 3; +} + +// CrawlerCriteriaAndHistory represents crawler information with criteria and +// history +message CrawlerCriteriaAndHistory { + // crawler_id: the ID of the crawler + string crawler_id = 1; + // platform: the platform from gravity_crawler_criteria + string platform = 2; + // topic: the topic from gravity_crawler_criteria + optional string topic = 3; + // keyword: the keyword from gravity_crawler_criteria + optional string keyword = 4; + // post_start_date: the start date for posts from gravity_crawler_criteria + optional google.protobuf.Timestamp post_start_date = 5; + // post_end_date: the end date for posts from gravity_crawler_criteria + optional google.protobuf.Timestamp post_end_date = 6; + // crawler_history: the history entries for this crawler + repeated CrawlerHistoryEntry crawler_history = 7; +} + +// GetCrawlerHistoryResponse is the response message for getting crawler history +message GetCrawlerHistoryResponse { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; + // crawlers: the crawlers with their criteria and history + repeated CrawlerCriteriaAndHistory crawlers = 2; +} + +// GetMarketplaceCrawlerDataForDDSubmissionRequest is the request message for getting crawler data for the marketplace user +message GetMarketplaceCrawlerDataForDDSubmissionRequest { + // marketplace_user_id: the ID of the marketplace user (required) + string marketplace_user_id = 1; +} + +// GetMarketplaceCrawlerDataForDDSubmissionResponse is the response message for marketplace crawler data +message GetMarketplaceCrawlerDataForDDSubmissionResponse { + // crawlers: list of marketplace crawler data for DD submission + repeated MarketplaceCrawlerDataForDDSubmission crawlers = 1; +} + +// MarketplaceCrawlerDataForDDSubmission contains crawler information for DD submission with all fields needed for UpsertDynamicDesirabilityEntry +message MarketplaceCrawlerDataForDDSubmission { + string crawler_id = 1; + string platform = 2; + optional string topic = 3; + optional string keyword = 4; + optional string post_start_datetime = 5; + optional string post_end_datetime = 6; + // Additional fields needed for UpsertDynamicDesirabilityEntry + google.protobuf.Timestamp start_time = 7; + google.protobuf.Timestamp deregistration_time = 8; + google.protobuf.Timestamp archive_time = 9; + string status = 10; + uint64 bytes_collected = 11; + uint64 records_collected = 12; + string notification_to = 13; + string notification_link = 14; + string user_id = 15; +} + +// GetActiveUserTasksResponse is the response message for active user tasks +message GetActiveUserTasksResponse { + // active_user_tasks: list of active user tasks + repeated ActiveUserTask active_user_tasks = 1; +} + +// ActiveUserCrawler contains active user crawler information +message ActiveUserCrawler { + // crawler_id: the id of the crawler + string crawler_id = 1; + // row_count: the number of rows collected by the crawler + uint64 row_count = 2; +} + +// ActiveUserTask contains active user task information +message ActiveUserTask { + // gravity_task_id: the id of the gravity_task + string gravity_task_id = 1; + // crawlers: list of active user crawlers + repeated ActiveUserCrawler crawlers = 2; +} + +// UpsertPreBuiltUserDatasetsRequest is the request message for upserting pre-built user datasets +message UpsertPreBuiltUserDatasetsRequest { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; + // crawler_id: the ID of the crawler + string crawler_id = 2; + // row_count: the number of rows in the pre-built dataset + int64 row_count = 3; +} + +// GetPreBuiltUserDatasetsRequest is the request message for getting pre-built user datasets +message GetPreBuiltUserDatasetsRequest { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; +} + +// PreBuiltUserDataset represents a single pre-built user dataset record +message PreBuiltUserDataset { + // gravity_task_id: the ID of the gravity task + string gravity_task_id = 1; + // crawler_id: the ID of the crawler + string crawler_id = 2; + // row_count: the number of rows in the pre-built dataset + int64 row_count = 3; +} + +// GetPreBuiltUserDatasetsResponse is the response message for getting pre-built user datasets +message GetPreBuiltUserDatasetsResponse { + // datasets: list of pre-built user datasets for the gravity task + repeated PreBuiltUserDataset datasets = 1; +} + + diff --git a/proto/sn13/v1/sn13_validator.proto b/proto/sn13/v1/sn13_validator.proto new file mode 100644 index 0000000..b226a6e --- /dev/null +++ b/proto/sn13/v1/sn13_validator.proto @@ -0,0 +1,89 @@ +syntax = "proto3"; + +package sn13.v1; +import "google/protobuf/struct.proto"; + +option go_package = "macrocosm-os/rift/constellation_api/gen/sn13/v1"; + + +service Sn13Service { + // ListTopics is the RPC method for getting the top topics + rpc ListTopics(ListTopicsRequest) returns (ListTopicsResponse); + rpc ValidateRedditTopic(ValidateRedditTopicRequest) returns (ValidateRedditTopicResponse); + + // Access the SN13 API endpoint on_demand_data_request via Constellation + rpc OnDemandData(OnDemandDataRequest) returns (OnDemandDataResponse); +} + +// ListTopicsRequest is the request message for getting the top topics +message ListTopicsRequest { + // source: the source to validate + string source = 1; +} + +// ListTopicsResponseDetail is the response message for getting the top topics +message ListTopicsResponseDetail { + // label_value: reddit or x topic + string label_value = 1; + // content_size_bytes: content size in bytes + uint64 content_size_bytes = 2; + // adj_content_size_bytes: adjacent content size in bytes + uint64 adj_content_size_bytes = 3; +} + +// ListTopicsResponse is a list of ListTopicsResponseDetail(s) with top topics +message ListTopicsResponse { + // message: the response message + repeated ListTopicsResponseDetail details = 1; +} + +// ValidateTopicRequest is the request message for validating a reddit topic +message ValidateRedditTopicRequest { + // topic: the topic to validate + string topic = 1; +} + +// ValidateTopicResponse is the response message for validating a topic +message ValidateRedditTopicResponse { + // platform: i.e. reddit + string platform = 1; + // topic: the topic to validate + string topic = 2; + // exists: whether the topic exists + bool exists = 3; + // over18: whether the topic is NSFW + bool over18 = 4; + // quarantine: whether the topic is quarantined + bool quarantine = 5; +} + +// OnDemandDataRequest is a request to SN13 to retrieve data +message OnDemandDataRequest { + // source: the data source (X, Reddit or Youtube) + string source = 1; + // usernames: list of usernames to fetch data from + repeated string usernames = 2; + // keywords: list of keywords to search for + repeated string keywords = 3; + // start_date: ISO 8601 formatted date string (e.g. "2024-01-01T00:00:00Z") + optional string start_date = 4; + // end_date: ISO 8601 formatted date string (e.g. "2024-01-31T23:59:59Z") + optional string end_date = 5; + // limit: maximum number of results to return + optional int64 limit = 6; + // keyword_mode: defines how keywords should be used in selecting response posts (optional): + // "all" (posts must include all keywords) or "any" (posts can include any combination of keywords) + optional string keyword_mode = 7; + // url: single URL for URL search mode (X) + optional string url = 8; +} + +// OnDemandDataResponse is the response from SN13 for an on-demand data request +message OnDemandDataResponse { + // status: the request status, either success/error + string status = 1; + // data: the data object returned + repeated google.protobuf.Struct data = 2; + // meta: additional metadata about the request + google.protobuf.Struct meta = 3; +} diff --git a/src/api/client.rs b/src/api/client.rs index 673fe16..ac104ab 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -1,57 +1,273 @@ -use anyhow::{bail, Context, Result}; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; +//! Native gRPC client for SN13 and Gravity APIs. + +use anyhow::{Context, Result}; use std::time::Duration; +use tonic::metadata::MetadataValue; +use tonic::transport::{Channel, ClientTlsConfig}; use super::types::*; pub const DEFAULT_BASE_URL: &str = "https://constellation.api.cloud.macrocosmos.ai"; -const SN13_SERVICE: &str = "sn13.v1.Sn13Service"; -const GRAVITY_SERVICE: &str = "gravity.v1.GravityService"; +// ─── Generated protobuf modules ───────────────────────────────────── + +pub mod sn13_proto { + tonic::include_proto!("sn13.v1"); +} + +pub mod gravity_proto { + tonic::include_proto!("gravity.v1"); +} + +// ─── Struct → JSON conversion ─────────────────────────────────────── + +fn struct_to_json(s: &prost_types::Struct) -> serde_json::Value { + let map: serde_json::Map = s + .fields + .iter() + .map(|(k, v)| (k.clone(), prost_value_to_json(v))) + .collect(); + serde_json::Value::Object(map) +} + +fn prost_value_to_json(v: &prost_types::Value) -> serde_json::Value { + match &v.kind { + Some(prost_types::value::Kind::NullValue(_)) => serde_json::Value::Null, + Some(prost_types::value::Kind::NumberValue(n)) => { + if *n == (*n as i64) as f64 && n.is_finite() { + serde_json::Value::Number(serde_json::Number::from(*n as i64)) + } else { + serde_json::Number::from_f64(*n) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null) + } + } + Some(prost_types::value::Kind::StringValue(s)) => serde_json::Value::String(s.clone()), + Some(prost_types::value::Kind::BoolValue(b)) => serde_json::Value::Bool(*b), + Some(prost_types::value::Kind::StructValue(s)) => struct_to_json(s), + Some(prost_types::value::Kind::ListValue(l)) => { + serde_json::Value::Array(l.values.iter().map(prost_value_to_json).collect()) + } + None => serde_json::Value::Null, + } +} + +// ─── Timestamp helpers ────────────────────────────────────────────── + +fn timestamp_to_string(ts: &prost_types::Timestamp) -> String { + // Convert to RFC 3339 style: YYYY-MM-DDTHH:MM:SSZ + let secs = ts.seconds; + // Use chrono-free approach: seconds since epoch -> date string + // We'll format as ISO 8601 + let dt = std::time::UNIX_EPOCH + Duration::from_secs(secs as u64); + let datetime: std::time::SystemTime = dt; + // Format using the humantime crate approach — just produce the timestamp + // Actually, let's just produce a simple format + humantime_format(datetime) +} + +fn humantime_format(t: std::time::SystemTime) -> String { + let dur = t + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default(); + let secs = dur.as_secs(); + + // Calculate date components + let days = secs / 86400; + let time_secs = secs % 86400; + let hours = time_secs / 3600; + let minutes = (time_secs % 3600) / 60; + let seconds = time_secs % 60; + + // Days since epoch to Y-M-D (simplified Gregorian) + let (year, month, day) = days_to_ymd(days as i64); + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + year, month, day, hours, minutes, seconds + ) +} + +fn days_to_ymd(mut days: i64) -> (i64, i64, i64) { + // Algorithm from https://howardhinnant.github.io/date_algorithms.html + days += 719468; + let era = if days >= 0 { days } else { days - 146096 } / 146097; + let doe = (days - era * 146097) as u64; // day of era + let yoe = + (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let y = yoe as i64 + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = if m <= 2 { y + 1 } else { y }; + (y, m as i64, d as i64) +} + +/// Parse an ISO 8601 date/datetime string into a prost_types::Timestamp. +fn parse_datetime_to_timestamp(s: &str) -> Option { + // Handle YYYY-MM-DD format + if s.len() == 10 { + let parts: Vec<&str> = s.split('-').collect(); + if parts.len() == 3 { + let year: i64 = parts[0].parse().ok()?; + let month: u32 = parts[1].parse().ok()?; + let day: u32 = parts[2].parse().ok()?; + let days = ymd_to_days(year, month, day); + return Some(prost_types::Timestamp { + seconds: days * 86400, + nanos: 0, + }); + } + } + // Handle ISO 8601 with T separator + if let Some(t_pos) = s.find('T') { + let date_part = &s[..t_pos]; + let time_part = s[t_pos + 1..].trim_end_matches('Z'); + let parts: Vec<&str> = date_part.split('-').collect(); + if parts.len() == 3 { + let year: i64 = parts[0].parse().ok()?; + let month: u32 = parts[1].parse().ok()?; + let day: u32 = parts[2].parse().ok()?; + let days = ymd_to_days(year, month, day); + + let time_parts: Vec<&str> = time_part.split(':').collect(); + let hours: i64 = time_parts.first()?.parse().ok()?; + let minutes: i64 = time_parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + let seconds: i64 = time_parts + .get(2) + .and_then(|s| s.split('.').next()?.parse().ok()) + .unwrap_or(0); + + return Some(prost_types::Timestamp { + seconds: days * 86400 + hours * 3600 + minutes * 60 + seconds, + nanos: 0, + }); + } + } + None +} + +fn ymd_to_days(year: i64, month: u32, day: u32) -> i64 { + // Inverse of days_to_ymd + let y = if month <= 2 { year - 1 } else { year }; + let m = if month <= 2 { + month as i64 + 9 + } else { + month as i64 - 3 + }; + let era = if y >= 0 { y } else { y - 399 } / 400; + let yoe = (y - era * 400) as u64; + let doy = (153 * m as u64 + 2) / 5 + day as u64 - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + era * 146097 + doe as i64 - 719468 +} + +// ─── Auth interceptor ─────────────────────────────────────────────── + +#[derive(Clone)] +struct AuthInterceptor { + api_key: String, +} + +impl tonic::service::Interceptor for AuthInterceptor { + fn call( + &mut self, + mut req: tonic::Request<()>, + ) -> std::result::Result, tonic::Status> { + let val: MetadataValue<_> = format!("Bearer {}", self.api_key) + .parse() + .map_err(|_| tonic::Status::internal("invalid api key"))?; + req.metadata_mut().insert("authorization", val); + + if let Ok(v) = "dataverse-rust-cli".parse() { + req.metadata_mut().insert("x-client-id", v); + } + + Ok(req) + } +} + +// ─── Error mapping ────────────────────────────────────────────────── + +fn map_grpc_error(status: tonic::Status) -> anyhow::Error { + match status.code() { + tonic::Code::Unauthenticated => { + anyhow::anyhow!( + "authentication failed: check your API key. {}", + status.message() + ) + } + tonic::Code::Unavailable => { + anyhow::anyhow!( + "service temporarily unavailable: {}\n Tip: the SN13 miner network may be busy. Retry in a few seconds.", + status.message() + ) + } + tonic::Code::Internal => { + anyhow::anyhow!( + "service temporarily unavailable (internal): {}\n Tip: the SN13 miner network may be busy. Retry in a few seconds.", + status.message() + ) + } + _ => anyhow::anyhow!("gRPC error ({}): {}", status.code(), status.message()), + } +} + +// ─── Type aliases for intercepted clients ─────────────────────────── + +type InterceptedChannel = + tonic::service::interceptor::InterceptedService; + +// ─── ApiClient ────────────────────────────────────────────────────── pub struct ApiClient { - http: reqwest::Client, - base_url: String, + sn13: sn13_proto::sn13_service_client::Sn13ServiceClient, + gravity: gravity_proto::gravity_service_client::GravityServiceClient, api_key: String, + base_url: String, } impl ApiClient { pub fn new(api_key: String, base_url: Option, timeout_secs: u64) -> Result { - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - headers.insert( - "x-client-id", - HeaderValue::from_static("dataverse-rust-cli"), - ); - headers.insert( - reqwest::header::USER_AGENT, - HeaderValue::from_str(&format!("dataverse-cli/{}", env!("CARGO_PKG_VERSION"))) - .expect("valid header value"), - ); - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {api_key}")) - .context("invalid API key for header")?, - ); + let url = base_url.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()); + + // We can't connect synchronously, so we create a lazy channel. + // tonic Channel::from_shared + connect_lazy avoids blocking here. + let tls = ClientTlsConfig::new().with_native_roots(); - let http = reqwest::Client::builder() - .default_headers(headers) + let channel = Channel::from_shared(url.clone()) + .context("invalid endpoint URL")? + .tls_config(tls) + .context("TLS config failed")? .timeout(Duration::from_secs(timeout_secs)) - .build() - .context("failed to build HTTP client")?; + .connect_lazy(); + + let interceptor = AuthInterceptor { + api_key: api_key.clone(), + }; + + let sn13 = sn13_proto::sn13_service_client::Sn13ServiceClient::with_interceptor( + channel.clone(), + interceptor.clone(), + ); + + let gravity = + gravity_proto::gravity_service_client::GravityServiceClient::with_interceptor( + channel, + interceptor, + ); Ok(Self { - http, - base_url: base_url.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()), + sn13, + gravity, api_key, + base_url: url, }) } - fn url(&self, service: &str, method: &str) -> String { - format!("{}/{}/{}", self.base_url, service, method) - } + // ─── Dry-run helpers ───────────────────────────────────────── - pub fn dry_run( + fn dry_run( &self, service: &str, method: &str, @@ -62,84 +278,125 @@ impl ApiClient { "authorization".to_string(), format!("Bearer {}", crate::config::Config::mask_key(&self.api_key)), ); - headers.insert("content-type".to_string(), "application/json".to_string()); + headers.insert( + "content-type".to_string(), + "application/grpc".to_string(), + ); headers.insert("x-client-id".to_string(), "dataverse-rust-cli".to_string()); DryRunOutput { - method: "POST".to_string(), - url: self.url(service, method), + method: "gRPC".to_string(), + url: format!("{}/{}/{}", self.base_url, service, method), headers, body: body.clone(), } } - async fn post( - &self, - service: &str, - method: &str, - body: &impl serde::Serialize, - ) -> Result { - let url = self.url(service, method); - let resp = self - .http - .post(&url) - .json(body) - .send() - .await - .with_context(|| format!("request to {url} failed"))?; - - let status = resp.status(); - if !status.is_success() { - let body_text = resp.text().await.unwrap_or_default(); - match status.as_u16() { - 401 => bail!("authentication failed: check your API key. {body_text}"), - 464 => { - // Macrocosmos custom status: upstream SN13 miner network issue - let detail = if body_text.is_empty() { - "SN13 miner network temporarily unavailable. Try again in a few seconds.".to_string() - } else { - body_text - }; - bail!("service unavailable (464): {detail}"); - } - 500 | 502 | 503 | 504 => { - let msg = if body_text.is_empty() { - "server error".to_string() - } else { - body_text - }; - bail!("service temporarily unavailable ({status}): {msg}\n Tip: the SN13 miner network may be busy. Retry in a few seconds."); - } - _ => bail!("API error {status}: {body_text}"), - } - } - - resp.json::() - .await - .with_context(|| format!("failed to parse response from {url}")) - } - - // ─── SN13 ─────────────────────────────────────────────────────── + // ─── SN13 ──────────────────────────────────────────────────── pub async fn on_demand_data( &self, req: &OnDemandDataRequest, ) -> Result { - self.post(SN13_SERVICE, "OnDemandData", req).await + let grpc_req = sn13_proto::OnDemandDataRequest { + source: req.source.clone(), + usernames: req.usernames.clone(), + keywords: req.keywords.clone(), + start_date: req.start_date.clone(), + end_date: req.end_date.clone(), + limit: req.limit, + keyword_mode: req.keyword_mode.clone(), + url: req.url.clone(), + }; + + let response = self + .sn13 + .clone() + .on_demand_data(tonic::Request::new(grpc_req)) + .await + .map_err(map_grpc_error)?; + + let inner = response.into_inner(); + + let data: Vec = inner.data.iter().map(struct_to_json).collect(); + let meta = inner.meta.as_ref().map(struct_to_json); + + // The gRPC server may return an empty status string on success; + // normalize to "success" so downstream checks work. + let status = if inner.status.is_empty() { + "success".to_string() + } else { + inner.status + }; + + Ok(OnDemandDataResponse { + status: Some(status), + data: Some(data), + meta, + }) } pub fn on_demand_data_dry_run(&self, req: &OnDemandDataRequest) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run(SN13_SERVICE, "OnDemandData", &body)) + Ok(self.dry_run("sn13.v1.Sn13Service", "OnDemandData", &body)) } - // ─── Gravity ──────────────────────────────────────────────────── + // ─── Gravity ───────────────────────────────────────────────── pub async fn create_gravity_task( &self, req: &CreateGravityTaskRequest, ) -> Result { - self.post(GRAVITY_SERVICE, "CreateGravityTask", req).await + let grpc_tasks: Vec = req + .gravity_tasks + .iter() + .map(|t| gravity_proto::GravityTask { + platform: t.platform.clone(), + topic: t.topic.clone(), + keyword: t.keyword.clone(), + post_start_datetime: t + .post_start_datetime + .as_ref() + .and_then(|s| parse_datetime_to_timestamp(s)), + post_end_datetime: t + .post_end_datetime + .as_ref() + .and_then(|s| parse_datetime_to_timestamp(s)), + }) + .collect(); + + let grpc_notifications: Vec = req + .notification_requests + .as_ref() + .map(|nrs| { + nrs.iter() + .map(|n| gravity_proto::NotificationRequest { + r#type: n.r#type.clone(), + address: n.address.clone(), + redirect_url: n.redirect_url.clone(), + }) + .collect() + }) + .unwrap_or_default(); + + let grpc_req = gravity_proto::CreateGravityTaskRequest { + gravity_tasks: grpc_tasks, + name: req.name.clone().unwrap_or_default(), + notification_requests: grpc_notifications, + gravity_task_id: None, + }; + + let response = self + .gravity + .clone() + .create_gravity_task(tonic::Request::new(grpc_req)) + .await + .map_err(map_grpc_error)?; + + let inner = response.into_inner(); + Ok(CreateGravityTaskResponse { + gravity_task_id: Some(inner.gravity_task_id), + }) } pub fn create_gravity_task_dry_run( @@ -147,14 +404,77 @@ impl ApiClient { req: &CreateGravityTaskRequest, ) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run(GRAVITY_SERVICE, "CreateGravityTask", &body)) + Ok(self.dry_run( + "gravity.v1.GravityService", + "CreateGravityTask", + &body, + )) } pub async fn get_gravity_tasks( &self, req: &GetGravityTasksRequest, ) -> Result { - self.post(GRAVITY_SERVICE, "GetGravityTasks", req).await + let grpc_req = gravity_proto::GetGravityTasksRequest { + gravity_task_id: req.gravity_task_id.clone(), + include_crawlers: req.include_crawlers, + }; + + let response = self + .gravity + .clone() + .get_gravity_tasks(tonic::Request::new(grpc_req)) + .await + .map_err(map_grpc_error)?; + + let inner = response.into_inner(); + + let states: Vec = inner + .gravity_task_states + .iter() + .map(|s| { + let crawler_workflows: Option> = + if s.crawler_workflows.is_empty() { + None + } else { + Some( + s.crawler_workflows + .iter() + .map(|c| crawler_to_json(c)) + .collect(), + ) + }; + + GravityTaskState { + gravity_task_id: if s.gravity_task_id.is_empty() { + None + } else { + Some(s.gravity_task_id.clone()) + }, + name: if s.name.is_empty() { + None + } else { + Some(s.name.clone()) + }, + status: if s.status.is_empty() { + None + } else { + Some(s.status.clone()) + }, + start_time: s.start_time.as_ref().map(timestamp_to_string), + crawler_ids: if s.crawler_ids.is_empty() { + None + } else { + Some(s.crawler_ids.clone()) + }, + crawler_workflows, + } + }) + .collect(); + + Ok(GetGravityTasksResponse { + gravity_task_states: Some(states), + }) } pub fn get_gravity_tasks_dry_run( @@ -162,43 +482,240 @@ impl ApiClient { req: &GetGravityTasksRequest, ) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run(GRAVITY_SERVICE, "GetGravityTasks", &body)) + Ok(self.dry_run( + "gravity.v1.GravityService", + "GetGravityTasks", + &body, + )) } pub async fn build_dataset( &self, req: &BuildDatasetRequest, ) -> Result { - self.post(GRAVITY_SERVICE, "BuildDataset", req).await + let grpc_notifications: Vec = req + .notification_requests + .as_ref() + .map(|nrs| { + nrs.iter() + .map(|n| gravity_proto::NotificationRequest { + r#type: n.r#type.clone(), + address: n.address.clone(), + redirect_url: n.redirect_url.clone(), + }) + .collect() + }) + .unwrap_or_default(); + + let grpc_req = gravity_proto::BuildDatasetRequest { + crawler_id: req.crawler_id.clone(), + notification_requests: grpc_notifications, + max_rows: req.max_rows.unwrap_or(10000), + is_periodic: None, + }; + + let response = self + .gravity + .clone() + .build_dataset(tonic::Request::new(grpc_req)) + .await + .map_err(map_grpc_error)?; + + let inner = response.into_inner(); + Ok(BuildDatasetResponse { + dataset_id: Some(inner.dataset_id), + dataset: inner.dataset.as_ref().map(dataset_to_json), + }) } pub fn build_dataset_dry_run(&self, req: &BuildDatasetRequest) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run(GRAVITY_SERVICE, "BuildDataset", &body)) + Ok(self.dry_run("gravity.v1.GravityService", "BuildDataset", &body)) } pub async fn get_dataset(&self, req: &GetDatasetRequest) -> Result { - self.post(GRAVITY_SERVICE, "GetDataset", req).await + let grpc_req = gravity_proto::GetDatasetRequest { + dataset_id: req.dataset_id.clone(), + }; + + let response = self + .gravity + .clone() + .get_dataset(tonic::Request::new(grpc_req)) + .await + .map_err(map_grpc_error)?; + + let inner = response.into_inner(); + + Ok(GetDatasetResponse { + dataset: inner.dataset.as_ref().map(proto_dataset_to_dataset_info), + }) } pub fn get_dataset_dry_run(&self, req: &GetDatasetRequest) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run(GRAVITY_SERVICE, "GetDataset", &body)) + Ok(self.dry_run("gravity.v1.GravityService", "GetDataset", &body)) } pub async fn cancel_gravity_task(&self, task_id: &str) -> Result { - let req = CancelRequest { - gravity_task_id: Some(task_id.to_string()), - dataset_id: None, + let grpc_req = gravity_proto::CancelGravityTaskRequest { + gravity_task_id: task_id.to_string(), }; - self.post(GRAVITY_SERVICE, "CancelGravityTask", &req).await + + let response = self + .gravity + .clone() + .cancel_gravity_task(tonic::Request::new(grpc_req)) + .await + .map_err(map_grpc_error)?; + + let inner = response.into_inner(); + Ok(CancelResponse { + message: Some(inner.message), + }) } pub async fn cancel_dataset(&self, dataset_id: &str) -> Result { - let req = CancelRequest { - gravity_task_id: None, - dataset_id: Some(dataset_id.to_string()), + let grpc_req = gravity_proto::CancelDatasetRequest { + dataset_id: dataset_id.to_string(), }; - self.post(GRAVITY_SERVICE, "CancelDataset", &req).await + + let response = self + .gravity + .clone() + .cancel_dataset(tonic::Request::new(grpc_req)) + .await + .map_err(map_grpc_error)?; + + let inner = response.into_inner(); + Ok(CancelResponse { + message: Some(inner.message), + }) + } +} + +// ─── Proto → serde type converters ────────────────────────────────── + +fn crawler_to_json(c: &gravity_proto::Crawler) -> serde_json::Value { + let mut map = serde_json::Map::new(); + map.insert( + "crawlerId".to_string(), + serde_json::Value::String(c.crawler_id.clone()), + ); + if let Some(state) = &c.state { + let mut state_map = serde_json::Map::new(); + state_map.insert( + "status".to_string(), + serde_json::Value::String(state.status.clone()), + ); + state_map.insert( + "bytesCollected".to_string(), + serde_json::Value::Number(serde_json::Number::from(state.bytes_collected)), + ); + state_map.insert( + "recordsCollected".to_string(), + serde_json::Value::Number(serde_json::Number::from(state.records_collected)), + ); + map.insert("state".to_string(), serde_json::Value::Object(state_map)); + } + if let Some(criteria) = &c.criteria { + let mut criteria_map = serde_json::Map::new(); + criteria_map.insert( + "platform".to_string(), + serde_json::Value::String(criteria.platform.clone()), + ); + if let Some(topic) = &criteria.topic { + criteria_map.insert( + "topic".to_string(), + serde_json::Value::String(topic.clone()), + ); + } + if let Some(keyword) = &criteria.keyword { + criteria_map.insert( + "keyword".to_string(), + serde_json::Value::String(keyword.clone()), + ); + } + map.insert( + "criteria".to_string(), + serde_json::Value::Object(criteria_map), + ); + } + if let Some(ts) = &c.start_time { + map.insert( + "startTime".to_string(), + serde_json::Value::String(timestamp_to_string(ts)), + ); + } + serde_json::Value::Object(map) +} + +fn dataset_to_json(d: &gravity_proto::Dataset) -> serde_json::Value { + serde_json::to_value(proto_dataset_to_dataset_info(d)).unwrap_or(serde_json::Value::Null) +} + +fn proto_dataset_to_dataset_info(d: &gravity_proto::Dataset) -> DatasetInfo { + DatasetInfo { + crawler_workflow_id: if d.crawler_workflow_id.is_empty() { + None + } else { + Some(d.crawler_workflow_id.clone()) + }, + create_date: d.create_date.as_ref().map(timestamp_to_string), + expire_date: d.expire_date.as_ref().map(timestamp_to_string), + files: if d.files.is_empty() { + None + } else { + Some( + d.files + .iter() + .map(|f| DatasetFile { + file_name: if f.file_name.is_empty() { + None + } else { + Some(f.file_name.clone()) + }, + file_size_bytes: Some(f.file_size_bytes as i64), + num_rows: Some(f.num_rows as i64), + url: if f.url.is_empty() { + None + } else { + Some(f.url.clone()) + }, + }) + .collect(), + ) + }, + status: if d.status.is_empty() { + None + } else { + Some(d.status.clone()) + }, + status_message: if d.status_message.is_empty() { + None + } else { + Some(d.status_message.clone()) + }, + steps: if d.steps.is_empty() { + None + } else { + Some( + d.steps + .iter() + .map(|s| DatasetStep { + progress: Some(s.progress), + step: Some(serde_json::Value::Number(serde_json::Number::from( + s.step, + ))), + step_name: if s.step_name.is_empty() { + None + } else { + Some(s.step_name.clone()) + }, + }) + .collect(), + ) + }, + total_steps: Some(d.total_steps), } } From b4730ad8e0f128034ecef69bb846c5b0be2ce7ce Mon Sep 17 00:00:00 2001 From: arrmlet Date: Sun, 22 Mar 2026 16:05:11 +0200 Subject: [PATCH 3/5] bump version to 0.1.4 Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4b7b0ba..9ca408f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dataverse-cli" -version = "0.1.3" +version = "0.1.4" edition = "2021" description = "CLI for Macrocosmos Data Universe (Bittensor SN13) - query social data from X/Twitter and Reddit" license = "MIT" From 19ec339e2a754f187119480205b42816258e6cbf Mon Sep 17 00:00:00 2001 From: arrmlet Date: Sun, 22 Mar 2026 16:15:02 +0200 Subject: [PATCH 4/5] Simplify: extract helpers, pre-compute auth, reduce allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review fixes from 3 parallel review agents: - Extract nonempty() helper — replaces 11 inline is_empty checks - Extract to_grpc_notifications() — deduplicates notification mapping - Restore SN13_SERVICE/GRAVITY_SERVICE/CLIENT_ID constants - Pre-compute Bearer header in AuthInterceptor (was formatting on every RPC) - Convert struct_to_json to use owned values (into_iter, no key cloning) - Replace 6 per-post is_reddit() calls with single PostFields::extract() - Fix truncate() double allocation for short strings - Guard negative timestamps in timestamp_to_string - Remove thinking-aloud comments Co-Authored-By: Claude Opus 4.6 (1M context) --- src/api/client.rs | 189 ++++++++++++++++++--------------------------- src/display/mod.rs | 107 +++++++++++-------------- 2 files changed, 123 insertions(+), 173 deletions(-) diff --git a/src/api/client.rs b/src/api/client.rs index ac104ab..8300cdb 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -8,6 +8,9 @@ use tonic::transport::{Channel, ClientTlsConfig}; use super::types::*; pub const DEFAULT_BASE_URL: &str = "https://constellation.api.cloud.macrocosmos.ai"; +const SN13_SERVICE: &str = "sn13.v1.Sn13Service"; +const GRAVITY_SERVICE: &str = "gravity.v1.GravityService"; +const CLIENT_ID: &str = "dataverse-rust-cli"; // ─── Generated protobuf modules ───────────────────────────────────── @@ -21,49 +24,67 @@ pub mod gravity_proto { // ─── Struct → JSON conversion ─────────────────────────────────────── -fn struct_to_json(s: &prost_types::Struct) -> serde_json::Value { +fn struct_to_json(s: prost_types::Struct) -> serde_json::Value { let map: serde_json::Map = s .fields - .iter() - .map(|(k, v)| (k.clone(), prost_value_to_json(v))) + .into_iter() + .map(|(k, v)| (k, prost_value_to_json(v))) .collect(); serde_json::Value::Object(map) } -fn prost_value_to_json(v: &prost_types::Value) -> serde_json::Value { - match &v.kind { +fn prost_value_to_json(v: prost_types::Value) -> serde_json::Value { + match v.kind { Some(prost_types::value::Kind::NullValue(_)) => serde_json::Value::Null, Some(prost_types::value::Kind::NumberValue(n)) => { - if *n == (*n as i64) as f64 && n.is_finite() { - serde_json::Value::Number(serde_json::Number::from(*n as i64)) + if n == (n as i64) as f64 && n.is_finite() { + serde_json::Value::Number(serde_json::Number::from(n as i64)) } else { - serde_json::Number::from_f64(*n) + serde_json::Number::from_f64(n) .map(serde_json::Value::Number) .unwrap_or(serde_json::Value::Null) } } - Some(prost_types::value::Kind::StringValue(s)) => serde_json::Value::String(s.clone()), - Some(prost_types::value::Kind::BoolValue(b)) => serde_json::Value::Bool(*b), + Some(prost_types::value::Kind::StringValue(s)) => serde_json::Value::String(s), + Some(prost_types::value::Kind::BoolValue(b)) => serde_json::Value::Bool(b), Some(prost_types::value::Kind::StructValue(s)) => struct_to_json(s), Some(prost_types::value::Kind::ListValue(l)) => { - serde_json::Value::Array(l.values.iter().map(prost_value_to_json).collect()) + serde_json::Value::Array(l.values.into_iter().map(prost_value_to_json).collect()) } None => serde_json::Value::Null, } } +/// Convert empty proto strings to None. +fn nonempty(s: &str) -> Option { + if s.is_empty() { None } else { Some(s.to_string()) } +} + +/// Convert notification requests from our types to proto types. +fn to_grpc_notifications( + reqs: &Option>, +) -> Vec { + reqs.as_ref() + .map(|nrs| { + nrs.iter() + .map(|n| gravity_proto::NotificationRequest { + r#type: n.r#type.clone(), + address: n.address.clone(), + redirect_url: n.redirect_url.clone(), + }) + .collect() + }) + .unwrap_or_default() +} + // ─── Timestamp helpers ────────────────────────────────────────────── fn timestamp_to_string(ts: &prost_types::Timestamp) -> String { - // Convert to RFC 3339 style: YYYY-MM-DDTHH:MM:SSZ - let secs = ts.seconds; - // Use chrono-free approach: seconds since epoch -> date string - // We'll format as ISO 8601 - let dt = std::time::UNIX_EPOCH + Duration::from_secs(secs as u64); - let datetime: std::time::SystemTime = dt; - // Format using the humantime crate approach — just produce the timestamp - // Actually, let's just produce a simple format - humantime_format(datetime) + if ts.seconds < 0 { + return String::new(); + } + let dt = std::time::UNIX_EPOCH + Duration::from_secs(ts.seconds as u64); + humantime_format(dt) } fn humantime_format(t: std::time::SystemTime) -> String { @@ -166,7 +187,20 @@ fn ymd_to_days(year: i64, month: u32, day: u32) -> i64 { #[derive(Clone)] struct AuthInterceptor { - api_key: String, + auth_header: MetadataValue, + client_id: MetadataValue, +} + +impl AuthInterceptor { + fn new(api_key: &str) -> Result { + let auth_header = format!("Bearer {api_key}") + .parse() + .map_err(|_| anyhow::anyhow!("invalid API key for header"))?; + let client_id = CLIENT_ID + .parse() + .map_err(|_| anyhow::anyhow!("invalid client ID"))?; + Ok(Self { auth_header, client_id }) + } } impl tonic::service::Interceptor for AuthInterceptor { @@ -174,15 +208,8 @@ impl tonic::service::Interceptor for AuthInterceptor { &mut self, mut req: tonic::Request<()>, ) -> std::result::Result, tonic::Status> { - let val: MetadataValue<_> = format!("Bearer {}", self.api_key) - .parse() - .map_err(|_| tonic::Status::internal("invalid api key"))?; - req.metadata_mut().insert("authorization", val); - - if let Ok(v) = "dataverse-rust-cli".parse() { - req.metadata_mut().insert("x-client-id", v); - } - + req.metadata_mut().insert("authorization", self.auth_header.clone()); + req.metadata_mut().insert("x-client-id", self.client_id.clone()); Ok(req) } } @@ -242,9 +269,7 @@ impl ApiClient { .timeout(Duration::from_secs(timeout_secs)) .connect_lazy(); - let interceptor = AuthInterceptor { - api_key: api_key.clone(), - }; + let interceptor = AuthInterceptor::new(&api_key)?; let sn13 = sn13_proto::sn13_service_client::Sn13ServiceClient::with_interceptor( channel.clone(), @@ -282,7 +307,7 @@ impl ApiClient { "content-type".to_string(), "application/grpc".to_string(), ); - headers.insert("x-client-id".to_string(), "dataverse-rust-cli".to_string()); + headers.insert("x-client-id".to_string(), CLIENT_ID.to_string()); DryRunOutput { method: "gRPC".to_string(), @@ -318,8 +343,8 @@ impl ApiClient { let inner = response.into_inner(); - let data: Vec = inner.data.iter().map(struct_to_json).collect(); - let meta = inner.meta.as_ref().map(struct_to_json); + let data: Vec = inner.data.into_iter().map(struct_to_json).collect(); + let meta = inner.meta.map(struct_to_json); // The gRPC server may return an empty status string on success; // normalize to "success" so downstream checks work. @@ -338,7 +363,7 @@ impl ApiClient { pub fn on_demand_data_dry_run(&self, req: &OnDemandDataRequest) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run("sn13.v1.Sn13Service", "OnDemandData", &body)) + Ok(self.dry_run(SN13_SERVICE, "OnDemandData", &body)) } // ─── Gravity ───────────────────────────────────────────────── @@ -365,19 +390,7 @@ impl ApiClient { }) .collect(); - let grpc_notifications: Vec = req - .notification_requests - .as_ref() - .map(|nrs| { - nrs.iter() - .map(|n| gravity_proto::NotificationRequest { - r#type: n.r#type.clone(), - address: n.address.clone(), - redirect_url: n.redirect_url.clone(), - }) - .collect() - }) - .unwrap_or_default(); + let grpc_notifications = to_grpc_notifications(&req.notification_requests); let grpc_req = gravity_proto::CreateGravityTaskRequest { gravity_tasks: grpc_tasks, @@ -405,7 +418,7 @@ impl ApiClient { ) -> Result { let body = serde_json::to_value(req)?; Ok(self.dry_run( - "gravity.v1.GravityService", + GRAVITY_SERVICE, "CreateGravityTask", &body, )) @@ -446,21 +459,9 @@ impl ApiClient { }; GravityTaskState { - gravity_task_id: if s.gravity_task_id.is_empty() { - None - } else { - Some(s.gravity_task_id.clone()) - }, - name: if s.name.is_empty() { - None - } else { - Some(s.name.clone()) - }, - status: if s.status.is_empty() { - None - } else { - Some(s.status.clone()) - }, + gravity_task_id: nonempty(&s.gravity_task_id), + name: nonempty(&s.name), + status: nonempty(&s.status), start_time: s.start_time.as_ref().map(timestamp_to_string), crawler_ids: if s.crawler_ids.is_empty() { None @@ -483,7 +484,7 @@ impl ApiClient { ) -> Result { let body = serde_json::to_value(req)?; Ok(self.dry_run( - "gravity.v1.GravityService", + GRAVITY_SERVICE, "GetGravityTasks", &body, )) @@ -493,19 +494,7 @@ impl ApiClient { &self, req: &BuildDatasetRequest, ) -> Result { - let grpc_notifications: Vec = req - .notification_requests - .as_ref() - .map(|nrs| { - nrs.iter() - .map(|n| gravity_proto::NotificationRequest { - r#type: n.r#type.clone(), - address: n.address.clone(), - redirect_url: n.redirect_url.clone(), - }) - .collect() - }) - .unwrap_or_default(); + let grpc_notifications = to_grpc_notifications(&req.notification_requests); let grpc_req = gravity_proto::BuildDatasetRequest { crawler_id: req.crawler_id.clone(), @@ -530,7 +519,7 @@ impl ApiClient { pub fn build_dataset_dry_run(&self, req: &BuildDatasetRequest) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run("gravity.v1.GravityService", "BuildDataset", &body)) + Ok(self.dry_run(GRAVITY_SERVICE, "BuildDataset", &body)) } pub async fn get_dataset(&self, req: &GetDatasetRequest) -> Result { @@ -554,7 +543,7 @@ impl ApiClient { pub fn get_dataset_dry_run(&self, req: &GetDatasetRequest) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run("gravity.v1.GravityService", "GetDataset", &body)) + Ok(self.dry_run(GRAVITY_SERVICE, "GetDataset", &body)) } pub async fn cancel_gravity_task(&self, task_id: &str) -> Result { @@ -656,11 +645,7 @@ fn dataset_to_json(d: &gravity_proto::Dataset) -> serde_json::Value { fn proto_dataset_to_dataset_info(d: &gravity_proto::Dataset) -> DatasetInfo { DatasetInfo { - crawler_workflow_id: if d.crawler_workflow_id.is_empty() { - None - } else { - Some(d.crawler_workflow_id.clone()) - }, + crawler_workflow_id: nonempty(&d.crawler_workflow_id), create_date: d.create_date.as_ref().map(timestamp_to_string), expire_date: d.expire_date.as_ref().map(timestamp_to_string), files: if d.files.is_empty() { @@ -670,32 +655,16 @@ fn proto_dataset_to_dataset_info(d: &gravity_proto::Dataset) -> DatasetInfo { d.files .iter() .map(|f| DatasetFile { - file_name: if f.file_name.is_empty() { - None - } else { - Some(f.file_name.clone()) - }, + file_name: nonempty(&f.file_name), file_size_bytes: Some(f.file_size_bytes as i64), num_rows: Some(f.num_rows as i64), - url: if f.url.is_empty() { - None - } else { - Some(f.url.clone()) - }, + url: nonempty(&f.url), }) .collect(), ) }, - status: if d.status.is_empty() { - None - } else { - Some(d.status.clone()) - }, - status_message: if d.status_message.is_empty() { - None - } else { - Some(d.status_message.clone()) - }, + status: nonempty(&d.status), + status_message: nonempty(&d.status_message), steps: if d.steps.is_empty() { None } else { @@ -707,11 +676,7 @@ fn proto_dataset_to_dataset_info(d: &gravity_proto::Dataset) -> DatasetInfo { step: Some(serde_json::Value::Number(serde_json::Number::from( s.step, ))), - step_name: if s.step_name.is_empty() { - None - } else { - Some(s.step_name.clone()) - }, + step_name: nonempty(&s.step_name), }) .collect(), ) diff --git a/src/display/mod.rs b/src/display/mod.rs index 6d0a7a5..21524ce 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -53,7 +53,7 @@ fn truncate(s: &str, max: usize) -> String { if chars.next().is_some() { format!("{collected}...") } else { - s.to_string() + collected } } @@ -104,62 +104,46 @@ fn is_reddit(post: &serde_json::Value) -> bool { ) } -/// Extract the display text for a post, handling both X (text) and Reddit (title + body). -fn post_text(post: &serde_json::Value) -> String { - if is_reddit(post) { - let title = extract_str(post, "title"); - let body = extract_str(post, "body"); - if title != "-" && body != "-" && !body.is_empty() { - format!("{title} | {body}") - } else if title != "-" { - title - } else { - body - } - } else { - extract_str(post, "text") - } -} - -/// Extract author for a post — Reddit uses top-level `username`, X uses `user.username`. -fn post_author(post: &serde_json::Value) -> String { - if is_reddit(post) { - extract_str(post, "username") - } else { - extract_str(post, "user.username") - } -} - -/// Extract engagement metrics based on source. -fn post_likes(post: &serde_json::Value) -> String { - if is_reddit(post) { - extract_num(post, "score") - } else { - extract_num(post, "tweet.like_count") - } -} - -fn post_reposts(post: &serde_json::Value) -> String { - if is_reddit(post) { - "-".to_string() - } else { - extract_num(post, "tweet.retweet_count") - } -} - -fn post_replies(post: &serde_json::Value) -> String { - if is_reddit(post) { - extract_num(post, "num_comments") - } else { - extract_num(post, "tweet.reply_count") - } +/// Extract all display fields for a post, checking source once. +struct PostFields { + text: String, + author: String, + likes: String, + reposts: String, + replies: String, + views: String, } -fn post_views(post: &serde_json::Value) -> String { - if is_reddit(post) { - "-".to_string() - } else { - extract_num(post, "tweet.view_count") +impl PostFields { + fn extract(post: &serde_json::Value) -> Self { + if is_reddit(post) { + let title = extract_str(post, "title"); + let body = extract_str(post, "body"); + let text = if title != "-" && body != "-" && !body.is_empty() { + format!("{title} | {body}") + } else if title != "-" { + title + } else { + body + }; + Self { + text, + author: extract_str(post, "username"), + likes: extract_num(post, "score"), + reposts: "-".to_string(), + replies: extract_num(post, "num_comments"), + views: "-".to_string(), + } + } else { + Self { + text: extract_str(post, "text"), + author: extract_str(post, "user.username"), + likes: extract_num(post, "tweet.like_count"), + reposts: extract_num(post, "tweet.retweet_count"), + replies: extract_num(post, "tweet.reply_count"), + views: extract_num(post, "tweet.view_count"), + } + } } } @@ -217,7 +201,8 @@ pub fn print_posts(data: &[serde_json::Value], format: OutputFormat) -> Result<( let rows: Vec = data .iter() .map(|post| { - let raw_text = post_text(post) + let fields = PostFields::extract(post); + let raw_text = fields.text .replace('\n', " ") .replace('\r', ""); PostRow { @@ -225,12 +210,12 @@ pub fn print_posts(data: &[serde_json::Value], format: OutputFormat) -> Result<( .chars() .take(16) .collect(), - author: truncate(&post_author(post), 20), + author: truncate(&fields.author, 20), text: truncate(&raw_text, 60), - likes: post_likes(post), - reposts: post_reposts(post), - replies: post_replies(post), - views: post_views(post), + likes: fields.likes, + reposts: fields.reposts, + replies: fields.replies, + views: fields.views, } }) .collect(); From 3f24143bfb143b7908f9476206b36db48d11a78c Mon Sep 17 00:00:00 2001 From: arrmlet Date: Sun, 22 Mar 2026 16:37:21 +0200 Subject: [PATCH 5/5] Hybrid transport: gRPC for SN13 search, HTTP for Gravity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gravity's gRPC endpoint is broken server-side — it sends JSON over the binary gRPC channel (compression flag 123 = '{' ASCII). SN13 search works perfectly via native gRPC. This simplifies the client to use each transport where it works: - SN13 (search): native gRPC via tonic (reliable, fast) - Gravity: HTTP/JSON via reqwest (their gRPC is broken) Removes ~400 lines of gravity proto conversion code (crawler_to_json, proto_dataset_to_dataset_info, timestamp helpers, etc.) since HTTP responses are already JSON and deserialize directly into our types. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 631 +++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + build.rs | 5 +- src/api/client.rs | 486 +++++------------------------------ 4 files changed, 688 insertions(+), 435 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0931e49..5c71789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "bytecount" version = "0.6.9" @@ -224,6 +230,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.6.0" @@ -331,7 +343,7 @@ dependencies = [ [[package]] name = "dataverse-cli" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "clap", @@ -341,6 +353,7 @@ dependencies = [ "dirs", "prost", "prost-types", + "reqwest", "serde", "serde_json", "tabled", @@ -384,6 +397,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "either" version = "1.15.0" @@ -442,6 +466,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -488,8 +521,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -500,7 +549,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -625,6 +674,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -644,13 +710,16 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.3", "tokio", @@ -658,12 +727,114 @@ dependencies = [ "tracing", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -686,6 +857,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -707,6 +894,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -734,12 +931,24 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchit" version = "0.7.3" @@ -876,6 +1085,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -978,6 +1196,61 @@ dependencies = [ "prost", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -987,6 +1260,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1000,8 +1279,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1011,7 +1300,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1023,6 +1322,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -1063,6 +1371,45 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "ring" version = "0.17.14" @@ -1077,6 +1424,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.1.4" @@ -1132,6 +1485,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -1248,6 +1602,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "shell-words" version = "1.1.1" @@ -1292,6 +1658,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -1331,6 +1703,20 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "tabled" @@ -1410,6 +1796,31 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -1569,7 +1980,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -1588,6 +1999,25 @@ dependencies = [ "futures-util", "pin-project-lite", "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -1665,6 +2095,24 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1725,6 +2173,65 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1759,6 +2266,35 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -1953,6 +2489,35 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.42" @@ -1973,12 +2538,66 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 9ca408f..120a82b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4", features = ["derive", "env"] } +reqwest = { version = "0.12", features = ["json", "rustls-tls", "http2"], default-features = false } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/build.rs b/build.rs index 06ffd82..3bbb669 100644 --- a/build.rs +++ b/build.rs @@ -2,10 +2,7 @@ fn main() -> Result<(), Box> { tonic_build::configure() .build_server(false) .compile_protos( - &[ - "proto/sn13/v1/sn13_validator.proto", - "proto/gravity/v1/gravity.proto", - ], + &["proto/sn13/v1/sn13_validator.proto"], &["proto"], )?; Ok(()) diff --git a/src/api/client.rs b/src/api/client.rs index 8300cdb..4778904 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -18,10 +18,6 @@ pub mod sn13_proto { tonic::include_proto!("sn13.v1"); } -pub mod gravity_proto { - tonic::include_proto!("gravity.v1"); -} - // ─── Struct → JSON conversion ─────────────────────────────────────── fn struct_to_json(s: prost_types::Struct) -> serde_json::Value { @@ -55,133 +51,6 @@ fn prost_value_to_json(v: prost_types::Value) -> serde_json::Value { } } -/// Convert empty proto strings to None. -fn nonempty(s: &str) -> Option { - if s.is_empty() { None } else { Some(s.to_string()) } -} - -/// Convert notification requests from our types to proto types. -fn to_grpc_notifications( - reqs: &Option>, -) -> Vec { - reqs.as_ref() - .map(|nrs| { - nrs.iter() - .map(|n| gravity_proto::NotificationRequest { - r#type: n.r#type.clone(), - address: n.address.clone(), - redirect_url: n.redirect_url.clone(), - }) - .collect() - }) - .unwrap_or_default() -} - -// ─── Timestamp helpers ────────────────────────────────────────────── - -fn timestamp_to_string(ts: &prost_types::Timestamp) -> String { - if ts.seconds < 0 { - return String::new(); - } - let dt = std::time::UNIX_EPOCH + Duration::from_secs(ts.seconds as u64); - humantime_format(dt) -} - -fn humantime_format(t: std::time::SystemTime) -> String { - let dur = t - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default(); - let secs = dur.as_secs(); - - // Calculate date components - let days = secs / 86400; - let time_secs = secs % 86400; - let hours = time_secs / 3600; - let minutes = (time_secs % 3600) / 60; - let seconds = time_secs % 60; - - // Days since epoch to Y-M-D (simplified Gregorian) - let (year, month, day) = days_to_ymd(days as i64); - format!( - "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", - year, month, day, hours, minutes, seconds - ) -} - -fn days_to_ymd(mut days: i64) -> (i64, i64, i64) { - // Algorithm from https://howardhinnant.github.io/date_algorithms.html - days += 719468; - let era = if days >= 0 { days } else { days - 146096 } / 146097; - let doe = (days - era * 146097) as u64; // day of era - let yoe = - (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - let y = yoe as i64 + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = doy - (153 * mp + 2) / 5 + 1; - let m = if mp < 10 { mp + 3 } else { mp - 9 }; - let y = if m <= 2 { y + 1 } else { y }; - (y, m as i64, d as i64) -} - -/// Parse an ISO 8601 date/datetime string into a prost_types::Timestamp. -fn parse_datetime_to_timestamp(s: &str) -> Option { - // Handle YYYY-MM-DD format - if s.len() == 10 { - let parts: Vec<&str> = s.split('-').collect(); - if parts.len() == 3 { - let year: i64 = parts[0].parse().ok()?; - let month: u32 = parts[1].parse().ok()?; - let day: u32 = parts[2].parse().ok()?; - let days = ymd_to_days(year, month, day); - return Some(prost_types::Timestamp { - seconds: days * 86400, - nanos: 0, - }); - } - } - // Handle ISO 8601 with T separator - if let Some(t_pos) = s.find('T') { - let date_part = &s[..t_pos]; - let time_part = s[t_pos + 1..].trim_end_matches('Z'); - let parts: Vec<&str> = date_part.split('-').collect(); - if parts.len() == 3 { - let year: i64 = parts[0].parse().ok()?; - let month: u32 = parts[1].parse().ok()?; - let day: u32 = parts[2].parse().ok()?; - let days = ymd_to_days(year, month, day); - - let time_parts: Vec<&str> = time_part.split(':').collect(); - let hours: i64 = time_parts.first()?.parse().ok()?; - let minutes: i64 = time_parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); - let seconds: i64 = time_parts - .get(2) - .and_then(|s| s.split('.').next()?.parse().ok()) - .unwrap_or(0); - - return Some(prost_types::Timestamp { - seconds: days * 86400 + hours * 3600 + minutes * 60 + seconds, - nanos: 0, - }); - } - } - None -} - -fn ymd_to_days(year: i64, month: u32, day: u32) -> i64 { - // Inverse of days_to_ymd - let y = if month <= 2 { year - 1 } else { year }; - let m = if month <= 2 { - month as i64 + 9 - } else { - month as i64 - 3 - }; - let era = if y >= 0 { y } else { y - 399 } / 400; - let yoe = (y - era * 400) as u64; - let doy = (153 * m as u64 + 2) / 5 + day as u64 - 1; - let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - era * 146097 + doe as i64 - 719468 -} // ─── Auth interceptor ─────────────────────────────────────────────── @@ -249,7 +118,7 @@ type InterceptedChannel = pub struct ApiClient { sn13: sn13_proto::sn13_service_client::Sn13ServiceClient, - gravity: gravity_proto::gravity_service_client::GravityServiceClient, + http: reqwest::Client, api_key: String, base_url: String, } @@ -258,10 +127,8 @@ impl ApiClient { pub fn new(api_key: String, base_url: Option, timeout_secs: u64) -> Result { let url = base_url.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()); - // We can't connect synchronously, so we create a lazy channel. - // tonic Channel::from_shared + connect_lazy avoids blocking here. + // SN13: native gRPC (reliable, bypasses ALB transcoding) let tls = ClientTlsConfig::new().with_native_roots(); - let channel = Channel::from_shared(url.clone()) .context("invalid endpoint URL")? .tls_config(tls) @@ -270,26 +137,61 @@ impl ApiClient { .connect_lazy(); let interceptor = AuthInterceptor::new(&api_key)?; - let sn13 = sn13_proto::sn13_service_client::Sn13ServiceClient::with_interceptor( - channel.clone(), - interceptor.clone(), + channel, + interceptor, ); - let gravity = - gravity_proto::gravity_service_client::GravityServiceClient::with_interceptor( - channel, - interceptor, - ); + // Gravity: HTTP/JSON (their gRPC endpoint is broken — sends JSON over binary channel) + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert(reqwest::header::CONTENT_TYPE, reqwest::header::HeaderValue::from_static("application/json")); + headers.insert("x-client-id", reqwest::header::HeaderValue::from_static(CLIENT_ID)); + headers.insert( + reqwest::header::AUTHORIZATION, + reqwest::header::HeaderValue::from_str(&format!("Bearer {api_key}")) + .context("invalid API key for header")?, + ); + let http = reqwest::Client::builder() + .default_headers(headers) + .timeout(Duration::from_secs(timeout_secs)) + .build() + .context("failed to build HTTP client")?; Ok(Self { sn13, - gravity, + http, api_key, base_url: url, }) } + /// HTTP POST for Gravity endpoints (their gRPC is broken server-side). + async fn gravity_post( + &self, + method: &str, + body: &impl serde::Serialize, + ) -> Result { + let url = format!("{}/{}/{}", self.base_url, GRAVITY_SERVICE, method); + let resp = self.http.post(&url).json(body).send().await + .with_context(|| format!("request to {url} failed"))?; + + let status = resp.status(); + if !status.is_success() { + let body_text = resp.text().await.unwrap_or_default(); + match status.as_u16() { + 401 => anyhow::bail!("authentication failed: check your API key. {body_text}"), + 500 | 502 | 503 | 504 => { + let msg = if body_text.is_empty() { "server error".to_string() } else { body_text }; + anyhow::bail!("service temporarily unavailable ({status}): {msg}\n Tip: the SN13 miner network may be busy. Retry in a few seconds."); + } + _ => anyhow::bail!("API error {status}: {body_text}"), + } + } + + resp.json::().await + .with_context(|| format!("failed to parse response from {url}")) + } + // ─── Dry-run helpers ───────────────────────────────────────── fn dry_run( @@ -366,155 +268,31 @@ impl ApiClient { Ok(self.dry_run(SN13_SERVICE, "OnDemandData", &body)) } - // ─── Gravity ───────────────────────────────────────────────── + // ─── Gravity (HTTP/JSON — their gRPC endpoint is broken) ──── pub async fn create_gravity_task( &self, req: &CreateGravityTaskRequest, ) -> Result { - let grpc_tasks: Vec = req - .gravity_tasks - .iter() - .map(|t| gravity_proto::GravityTask { - platform: t.platform.clone(), - topic: t.topic.clone(), - keyword: t.keyword.clone(), - post_start_datetime: t - .post_start_datetime - .as_ref() - .and_then(|s| parse_datetime_to_timestamp(s)), - post_end_datetime: t - .post_end_datetime - .as_ref() - .and_then(|s| parse_datetime_to_timestamp(s)), - }) - .collect(); - - let grpc_notifications = to_grpc_notifications(&req.notification_requests); - - let grpc_req = gravity_proto::CreateGravityTaskRequest { - gravity_tasks: grpc_tasks, - name: req.name.clone().unwrap_or_default(), - notification_requests: grpc_notifications, - gravity_task_id: None, - }; - - let response = self - .gravity - .clone() - .create_gravity_task(tonic::Request::new(grpc_req)) - .await - .map_err(map_grpc_error)?; - - let inner = response.into_inner(); - Ok(CreateGravityTaskResponse { - gravity_task_id: Some(inner.gravity_task_id), - }) + self.gravity_post("CreateGravityTask", req).await } - pub fn create_gravity_task_dry_run( - &self, - req: &CreateGravityTaskRequest, - ) -> Result { + pub fn create_gravity_task_dry_run(&self, req: &CreateGravityTaskRequest) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run( - GRAVITY_SERVICE, - "CreateGravityTask", - &body, - )) + Ok(self.dry_run(GRAVITY_SERVICE, "CreateGravityTask", &body)) } - pub async fn get_gravity_tasks( - &self, - req: &GetGravityTasksRequest, - ) -> Result { - let grpc_req = gravity_proto::GetGravityTasksRequest { - gravity_task_id: req.gravity_task_id.clone(), - include_crawlers: req.include_crawlers, - }; - - let response = self - .gravity - .clone() - .get_gravity_tasks(tonic::Request::new(grpc_req)) - .await - .map_err(map_grpc_error)?; - - let inner = response.into_inner(); - - let states: Vec = inner - .gravity_task_states - .iter() - .map(|s| { - let crawler_workflows: Option> = - if s.crawler_workflows.is_empty() { - None - } else { - Some( - s.crawler_workflows - .iter() - .map(|c| crawler_to_json(c)) - .collect(), - ) - }; - - GravityTaskState { - gravity_task_id: nonempty(&s.gravity_task_id), - name: nonempty(&s.name), - status: nonempty(&s.status), - start_time: s.start_time.as_ref().map(timestamp_to_string), - crawler_ids: if s.crawler_ids.is_empty() { - None - } else { - Some(s.crawler_ids.clone()) - }, - crawler_workflows, - } - }) - .collect(); - - Ok(GetGravityTasksResponse { - gravity_task_states: Some(states), - }) + pub async fn get_gravity_tasks(&self, req: &GetGravityTasksRequest) -> Result { + self.gravity_post("GetGravityTasks", req).await } - pub fn get_gravity_tasks_dry_run( - &self, - req: &GetGravityTasksRequest, - ) -> Result { + pub fn get_gravity_tasks_dry_run(&self, req: &GetGravityTasksRequest) -> Result { let body = serde_json::to_value(req)?; - Ok(self.dry_run( - GRAVITY_SERVICE, - "GetGravityTasks", - &body, - )) + Ok(self.dry_run(GRAVITY_SERVICE, "GetGravityTasks", &body)) } - pub async fn build_dataset( - &self, - req: &BuildDatasetRequest, - ) -> Result { - let grpc_notifications = to_grpc_notifications(&req.notification_requests); - - let grpc_req = gravity_proto::BuildDatasetRequest { - crawler_id: req.crawler_id.clone(), - notification_requests: grpc_notifications, - max_rows: req.max_rows.unwrap_or(10000), - is_periodic: None, - }; - - let response = self - .gravity - .clone() - .build_dataset(tonic::Request::new(grpc_req)) - .await - .map_err(map_grpc_error)?; - - let inner = response.into_inner(); - Ok(BuildDatasetResponse { - dataset_id: Some(inner.dataset_id), - dataset: inner.dataset.as_ref().map(dataset_to_json), - }) + pub async fn build_dataset(&self, req: &BuildDatasetRequest) -> Result { + self.gravity_post("BuildDataset", req).await } pub fn build_dataset_dry_run(&self, req: &BuildDatasetRequest) -> Result { @@ -523,22 +301,7 @@ impl ApiClient { } pub async fn get_dataset(&self, req: &GetDatasetRequest) -> Result { - let grpc_req = gravity_proto::GetDatasetRequest { - dataset_id: req.dataset_id.clone(), - }; - - let response = self - .gravity - .clone() - .get_dataset(tonic::Request::new(grpc_req)) - .await - .map_err(map_grpc_error)?; - - let inner = response.into_inner(); - - Ok(GetDatasetResponse { - dataset: inner.dataset.as_ref().map(proto_dataset_to_dataset_info), - }) + self.gravity_post("GetDataset", req).await } pub fn get_dataset_dry_run(&self, req: &GetDatasetRequest) -> Result { @@ -547,140 +310,13 @@ impl ApiClient { } pub async fn cancel_gravity_task(&self, task_id: &str) -> Result { - let grpc_req = gravity_proto::CancelGravityTaskRequest { - gravity_task_id: task_id.to_string(), - }; - - let response = self - .gravity - .clone() - .cancel_gravity_task(tonic::Request::new(grpc_req)) - .await - .map_err(map_grpc_error)?; - - let inner = response.into_inner(); - Ok(CancelResponse { - message: Some(inner.message), - }) + let req = CancelRequest { gravity_task_id: Some(task_id.to_string()), dataset_id: None }; + self.gravity_post("CancelGravityTask", &req).await } pub async fn cancel_dataset(&self, dataset_id: &str) -> Result { - let grpc_req = gravity_proto::CancelDatasetRequest { - dataset_id: dataset_id.to_string(), - }; - - let response = self - .gravity - .clone() - .cancel_dataset(tonic::Request::new(grpc_req)) - .await - .map_err(map_grpc_error)?; - - let inner = response.into_inner(); - Ok(CancelResponse { - message: Some(inner.message), - }) - } -} - -// ─── Proto → serde type converters ────────────────────────────────── - -fn crawler_to_json(c: &gravity_proto::Crawler) -> serde_json::Value { - let mut map = serde_json::Map::new(); - map.insert( - "crawlerId".to_string(), - serde_json::Value::String(c.crawler_id.clone()), - ); - if let Some(state) = &c.state { - let mut state_map = serde_json::Map::new(); - state_map.insert( - "status".to_string(), - serde_json::Value::String(state.status.clone()), - ); - state_map.insert( - "bytesCollected".to_string(), - serde_json::Value::Number(serde_json::Number::from(state.bytes_collected)), - ); - state_map.insert( - "recordsCollected".to_string(), - serde_json::Value::Number(serde_json::Number::from(state.records_collected)), - ); - map.insert("state".to_string(), serde_json::Value::Object(state_map)); - } - if let Some(criteria) = &c.criteria { - let mut criteria_map = serde_json::Map::new(); - criteria_map.insert( - "platform".to_string(), - serde_json::Value::String(criteria.platform.clone()), - ); - if let Some(topic) = &criteria.topic { - criteria_map.insert( - "topic".to_string(), - serde_json::Value::String(topic.clone()), - ); - } - if let Some(keyword) = &criteria.keyword { - criteria_map.insert( - "keyword".to_string(), - serde_json::Value::String(keyword.clone()), - ); - } - map.insert( - "criteria".to_string(), - serde_json::Value::Object(criteria_map), - ); + let req = CancelRequest { gravity_task_id: None, dataset_id: Some(dataset_id.to_string()) }; + self.gravity_post("CancelDataset", &req).await } - if let Some(ts) = &c.start_time { - map.insert( - "startTime".to_string(), - serde_json::Value::String(timestamp_to_string(ts)), - ); - } - serde_json::Value::Object(map) } -fn dataset_to_json(d: &gravity_proto::Dataset) -> serde_json::Value { - serde_json::to_value(proto_dataset_to_dataset_info(d)).unwrap_or(serde_json::Value::Null) -} - -fn proto_dataset_to_dataset_info(d: &gravity_proto::Dataset) -> DatasetInfo { - DatasetInfo { - crawler_workflow_id: nonempty(&d.crawler_workflow_id), - create_date: d.create_date.as_ref().map(timestamp_to_string), - expire_date: d.expire_date.as_ref().map(timestamp_to_string), - files: if d.files.is_empty() { - None - } else { - Some( - d.files - .iter() - .map(|f| DatasetFile { - file_name: nonempty(&f.file_name), - file_size_bytes: Some(f.file_size_bytes as i64), - num_rows: Some(f.num_rows as i64), - url: nonempty(&f.url), - }) - .collect(), - ) - }, - status: nonempty(&d.status), - status_message: nonempty(&d.status_message), - steps: if d.steps.is_empty() { - None - } else { - Some( - d.steps - .iter() - .map(|s| DatasetStep { - progress: Some(s.progress), - step: Some(serde_json::Value::Number(serde_json::Number::from( - s.step, - ))), - step_name: nonempty(&s.step_name), - }) - .collect(), - ) - }, - total_steps: Some(d.total_steps), - } -}