From 6ab5289452e34269a6e58957adb2b73b5390328f Mon Sep 17 00:00:00 2001 From: Marius Cobzarenco Date: Fri, 26 Jun 2020 21:01:58 +0100 Subject: [PATCH] Open source reinfer cli and Rust API library --- .gitignore | 2 + Cargo.lock | 1737 +++++++++++++++++++++++++++ Cargo.toml | 9 + Dockerfile | 39 + README.md | 159 +++ api/.gitignore | 2 + api/Cargo.toml | 19 + api/README.md | 3 + api/src/errors.rs | 77 ++ api/src/lib.rs | 1031 ++++++++++++++++ api/src/resources/bucket.rs | 155 +++ api/src/resources/comment.rs | 644 ++++++++++ api/src/resources/dataset.rs | 116 ++ api/src/resources/email.rs | 49 + api/src/resources/mod.rs | 81 ++ api/src/resources/source.rs | 128 ++ api/src/resources/statistics.rs | 11 + api/src/resources/trigger.rs | 105 ++ api/src/resources/user.rs | 250 ++++ cargo-audit-known-issues.list | 4 + check | 7 + cli/.gitignore | 2 + cli/Cargo.toml | 29 + cli/README.md | 1 + cli/convert-comments.py | 85 ++ cli/publish-binaries | 25 + cli/readme-demo.gif | Bin 0 -> 493215 bytes cli/src/args.rs | 99 ++ cli/src/commands/config.rs | 264 ++++ cli/src/commands/create/bucket.rs | 45 + cli/src/commands/create/comments.rs | 328 +++++ cli/src/commands/create/dataset.rs | 79 ++ cli/src/commands/create/emails.rs | 208 ++++ cli/src/commands/create/mod.rs | 53 + cli/src/commands/create/source.rs | 79 ++ cli/src/commands/create/user.rs | 71 ++ cli/src/commands/delete.rs | 57 + cli/src/commands/get.rs | 705 +++++++++++ cli/src/commands/mod.rs | 34 + cli/src/config.rs | 127 ++ cli/src/errors.rs | 27 + cli/src/main.rs | 148 +++ cli/src/progress.rs | 130 ++ cli/src/utils.rs | 123 ++ pyproject.toml | 1 + readme-demo.gif | Bin 0 -> 493215 bytes 46 files changed, 7348 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 api/.gitignore create mode 100644 api/Cargo.toml create mode 100644 api/README.md create mode 100644 api/src/errors.rs create mode 100644 api/src/lib.rs create mode 100644 api/src/resources/bucket.rs create mode 100644 api/src/resources/comment.rs create mode 100644 api/src/resources/dataset.rs create mode 100644 api/src/resources/email.rs create mode 100644 api/src/resources/mod.rs create mode 100644 api/src/resources/source.rs create mode 100644 api/src/resources/statistics.rs create mode 100644 api/src/resources/trigger.rs create mode 100644 api/src/resources/user.rs create mode 100644 cargo-audit-known-issues.list create mode 100755 check create mode 100644 cli/.gitignore create mode 100644 cli/Cargo.toml create mode 120000 cli/README.md create mode 100755 cli/convert-comments.py create mode 100755 cli/publish-binaries create mode 100644 cli/readme-demo.gif create mode 100644 cli/src/args.rs create mode 100644 cli/src/commands/config.rs create mode 100644 cli/src/commands/create/bucket.rs create mode 100644 cli/src/commands/create/comments.rs create mode 100644 cli/src/commands/create/dataset.rs create mode 100644 cli/src/commands/create/emails.rs create mode 100644 cli/src/commands/create/mod.rs create mode 100644 cli/src/commands/create/source.rs create mode 100644 cli/src/commands/create/user.rs create mode 100644 cli/src/commands/delete.rs create mode 100644 cli/src/commands/get.rs create mode 100644 cli/src/commands/mod.rs create mode 100644 cli/src/config.rs create mode 100644 cli/src/errors.rs create mode 100644 cli/src/main.rs create mode 100644 cli/src/progress.rs create mode 100644 cli/src/utils.rs create mode 120000 pyproject.toml create mode 100644 readme-demo.gif diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..53eaa219 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..826634a4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1737 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "adler32" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" + +[[package]] +name = "aho-corasick" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + +[[package]] +name = "async-compression" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae84766bab9f774e32979583ba56d6af8c701288c6dc99144819d5d2ee0b170f" +dependencies = [ + "bytes", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "backtrace" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ed64ae6d9ebfd9893193c4b2532b1292ec97bd8271c9d7d0fa90cd78a34cba" +dependencies = [ + "backtrace-sys", + "cfg-if", + "libc", + "rustc-demangle", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fbebbe1c9d1f383a9cc7e8ccdb471b91c8d024ee9c2ca5b5346121fe8b4399" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e223af0dc48c96d4f8342ec01a4974f139df863896b316681efd36742f22cc67" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "bstr" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "bytes" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b" +dependencies = [ + "loom", +] + +[[package]] +name = "cc" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" + +[[package]] +name = "cfg-if" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" + +[[package]] +name = "chrono" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" +dependencies = [ + "num-integer", + "num-traits", + "serde", + "time", +] + +[[package]] +name = "clap" +version = "2.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" +dependencies = [ + "bitflags", + "textwrap", + "unicode-width", +] + +[[package]] +name = "colored" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" +dependencies = [ + "atty", + "lazy_static", + "winapi 0.3.8", +] + +[[package]] +name = "console" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c0994e656bba7b922d8dd1245db90672ffb701e684e45be58f20719d69abc5a" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "termios", + "unicode-width", + "winapi 0.3.8", + "winapi-util", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "csv" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.8", +] + +[[package]] +name = "dirs" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fddc3610d8f9552384e06ebc87f714e1d0b2b64a99194d2faf36d7ae5f48549" +dependencies = [ + "cfg-if", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.8", +] + +[[package]] +name = "dtoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "failchain" +version = "0.1015.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9482e7e494fd3f3922c3de23788c656dde1bd039824669be7bc53b2551ed770f" +dependencies = [ + "failure", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "flate2" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures-channel" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" + +[[package]] +name = "futures-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" + +[[package]] +name = "futures-sink" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" + +[[package]] +name = "futures-task" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68" +dependencies = [ + "cc", + "libc", + "log", + "rustc_version", + "winapi 0.3.8", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "h2" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "log", + "slab", + "tokio", + "tokio-util", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "hyper" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e7655b9594024ad0ee439f3b5a7299369dc2a3f459b47c696f9ff676f9aa1f" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "log", + "pin-project", + "socket2", + "time", + "tokio", + "tower-service", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-tls", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe" +dependencies = [ + "autocfg", +] + +[[package]] +name = "indicatif" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7baab56125e25686df467fe470785512329883aab42696d661247aca2a2896e4" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "js-sys" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce10c23ad2ea25ceca0093bd3192229da4c5b3c0f2de499c1ecac0d98d452177" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "loom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "mio" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +dependencies = [ + "cfg-if", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" +dependencies = [ + "cfg-if", + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" + +[[package]] +name = "once_cell" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" + +[[package]] +name = "openssl" +version = "0.10.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-src" +version = "111.10.0+1.1.1g" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47cd4a96d49c3abf4cac8e8a80cba998a030c75608f158fb1c5f609772f265e6" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +dependencies = [ + "autocfg", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12e3a6cdbfe94a5e4572812a0201f8c0ed98c1c452c7b8563ce2276988ef9c17" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ffd45cf79d88737d7cc85bfd5d2894bee1139b356e616fe85dc389c61aaf7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" + +[[package]] +name = "ppv-lite86" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" + +[[package]] +name = "prettytable-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" +dependencies = [ + "atty", + "csv", + "encode_unicode", + "lazy_static", + "term", + "unicode-width", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "syn-mid", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "redox_users" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "regex" +version = "1.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-automata" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" +dependencies = [ + "byteorder", +] + +[[package]] +name = "regex-syntax" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" + +[[package]] +name = "reinfer-cli" +version = "0.2.3" +dependencies = [ + "chrono", + "colored", + "dirs 3.0.0", + "env_logger", + "failchain", + "failure", + "indicatif", + "lazy_static", + "log", + "maplit", + "prettytable-rs", + "reinfer-client", + "reqwest", + "serde", + "serde_json", + "structopt", +] + +[[package]] +name = "reinfer-client" +version = "0.2.0" +dependencies = [ + "chrono", + "failchain", + "failure", + "failure_derive", + "lazy_static", + "log", + "reqwest", + "serde", + "serde_json", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.8", +] + +[[package]] +name = "reqwest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b82c9238b305f26f53443e3a4bc8528d64b8d0bee408ec949eb7bf5635ec680" +dependencies = [ + "async-compression", + "base64 0.12.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rust-argon2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" +dependencies = [ + "base64 0.11.0", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.8", +] + +[[package]] +name = "scoped-tls" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" + +[[package]] +name = "security-framework" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2c5d7e739bc07a3e73381a39d61fdb5f671c60c1df26a130690665803d8226" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" +dependencies = [ + "dtoa", + "itoa", + "serde", + "url", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.8", +] + +[[package]] +name = "structopt" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2f5e239ee807089b62adce73e48c625e0ed80df02c7ab3f068f5db5281065c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510413f9de616762a4fbeab62509bf15c729603b72d7cd71280fbca431b1c118" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.8", +] + +[[package]] +name = "term" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" +dependencies = [ + "byteorder", + "dirs 1.0.5", + "winapi 0.3.8", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8038f95fc7a6f351163f4b964af631bd26c9e828f7db085f2a84aca56f70d13b" +dependencies = [ + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "termios" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2" +dependencies = [ + "libc", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "tinyvec" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" + +[[package]] +name = "tokio" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "slab", +] + +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" + +[[package]] +name = "try-lock" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" + +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +dependencies = [ + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2dc4aa152834bc334f506c1a06b866416a8b6697d5c9f75b9a689c8486def0" +dependencies = [ + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded84f06e0ed21499f6184df0e0cb3494727b0c5da89534e0fcc55c51d812101" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64487204d863f109eb77e8462189d111f27cb5712cc9fdb3461297a76963a2f6" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "838e423688dac18d73e31edce74ddfac468e37b1506ad163ffaf0a46f703ffe3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3156052d8ec77142051a533cdd686cba889537b213f948cd1d20869926e68e92" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ba19973a58daf4db6f352eda73dc0e289493cd29fb2632eb172085b6521acd" + +[[package]] +name = "web-sys" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b72fe77fd39e4bd3eaa4412fd299a0be6b3dfe9d2597e2f1c20beb968f41d17" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.8", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.8", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..0c7650a9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +members = [ + "api", + "cli", +] + + +[profile.release] +panic="abort" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2eb5a2d4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# syntax=docker/dockerfile:experimental +# !!! This won't build with docker-compose due to BUILDKIT !!! +# You can build it using (from the root of the repo): +# docker build -f client/Dockerfile -t reinfer-dev/client . +ARG REINFER_BASE_IMAGE_TAG=latest +FROM eu.gcr.io/reinfer-gcr/builder:${REINFER_BASE_IMAGE_TAG} as builder + +COPY ci/rshelp /src/ci/rshelp +COPY client /src/client + +# Note that `PROJECT_PATH` needs to be updated here, but also in the volume on the first RUN line, +# env/arg expansion in --mount commands is not supported by docker yet: https://github.com/moby/buildkit/issues/815 +ENV PROJECT_PATH="client" +WORKDIR "/src/${PROJECT_PATH}" + +ARG SCCACHE_REDIS +ENV PKG_CONFIG_ALLOW_CROSS=1 +RUN --mount=type=cache,id=cargo-git,target=/root/.cargo/git,sharing=locked \ + --mount=type=cache,id=cargo-registry,target=/root/.cargo/registry,sharing=locked \ + --mount=type=cache,id=client-target,target=/src/client/target \ + /src/ci/rshelp/rs-build && \ + cargo build --locked --release --target x86_64-unknown-linux-musl && \ + # So this is kinda ugly, but we want to keep the volumes around for \ + # `cargo test`, so we need to put them somewhere else then link back to \ + # them with the volumes unmounted. \ + mkdir -p /build/${PROJECT_PATH} && \ + cp -ar /src/${PROJECT_PATH}/target /build/${PROJECT_PATH}/target && \ + cp -ar /root/.cargo/git /build/cargo-git && \ + cp -ar /root/.cargo/registry /build/cargo-registry + +RUN rmdir /src/${PROJECT_PATH}/target /root/.cargo/git /root/.cargo/registry && \ + ln -s /build/${PROJECT_PATH}/target /src/${PROJECT_PATH}/ && \ + ln -s /build/cargo-git /root/.cargo/git && \ + ln -s /build/cargo-registry /root/.cargo/registry + +ARG REINFER_BASE_IMAGE_TAG=latest +FROM eu.gcr.io/reinfer-gcr/rs:${REINFER_BASE_IMAGE_TAG} +COPY --from=builder /build/client/target/x86_64-unknown-linux-musl/release/re /usr/bin/re +ENTRYPOINT [ "/usr/bin/tini", "--", "/usr/bin/re" ] diff --git a/README.md b/README.md new file mode 100644 index 00000000..2747b537 --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +# reinfer/cli + +`re` is the command line interface for [reinfer](https://reinfer.io). It simplifies managing reinfer resources, such as sources and datasets, as well as importing or exporting communications data. Additionally, `re` maintains multiple contexts, making it easy to switch between multiple authentication tokens for different users (and endpoints for multiple clusters if needed). + +#### API Library + +The [api](/api) directory contains a Rust client library for reinfer which can be used for API access independently. Please refer to that directory for more information. The rest of the README is about the command line tool for managing reinfer resources. + +### Features + +- _Create, get, update and delete_ operations for sources, datasets, comments and more. +- Context management for multiple endpoints (reinfer clusters) and user tokens. +- Upload new verbatims to a source. +- Easily download raw verbatims from a set of sources and datasets together + with human applied annotations. Useful for backups, migrating data or for + applying some transformations to the data. +- Basic shell autocompletion for `zsh` and `bash`. +- Colorized terminal output and progress bars. + +### Demo + +![](/client/readme-demo.gif) + +## Installation + +### Debian / Ubuntu + +You can download a `.deb` package [here](https://reinfer.io/public/cli/debian/reinfer-cli_0.2.1_amd64.deb). + +### Binary + +You can download binaries for your platform below: + +- [x86_64-unknown-linux-musl](https://reinfer.io/public/cli/bin/x86_64-unknown-linux-musl/0.2.1/re) (statically linked) +- [x86_64-unknown-linux](https://reinfer.io/public/cli/bin/x86_64-unknown-linux/0.2.1/re) (dynamically linked) + +### From Source + +To build from source, you need a recent version of the [Rust toolchain](https://rustup.rs/) installed. + +#### Using `cargo install` + +To install using `cargo install` run the following. + +``` +cd cli +cargo install --force --path . reinfer-cli +``` + +Ensure you have the cargo bin directory in your path (typically `~/.cargo/bin`). + +#### Manual + +Build it the usual way using cargo + +``` +cargo build --release +``` + +The binary is located at `../target/release/re`. Move it somewhere suitable, e.g. + +``` +sudo mv ../target/release/re /usr/local/bin/ +``` + +## Getting Started + +Check the installation and see a full listing of the available commands by running `re`. + +### Authentication + +#### Per Session + +The simplest way to authenticate is to specify the API token for every command. By default `re` will prompt you to enter it interactively. E.g. to list the available datasets + +``` +➜ re get datasets +input: Enter API token [none]: MYSUPERSECRETAPITOKEN + Name ID Updated (UTC) Title + InvestmentBank/collateral-triggers aa9dda7c059e5a8d 2019-04-30 17:25:03 IB Collateral Triggers + InvestmentBank/george-test 1aaeacd49dfce8a0 2019-05-10 15:32:34 Test Dataset + InvestmentBank/margin-call b9d50fb2b38c3af5 2019-05-08 07:51:09 IB Margin Call + InvestmentBank/margin-call-large 6d00b9f69ab059f6 2019-05-11 09:23:43 IB Margin Call Large +``` + +The token can also be specified using `--token` + +``` +➜ re --token MYSUPERSECRETAPITOKEN get datasets +``` + +This is not generally a good idea (e.g. it'll be stored in your shell history). Better to store in a environment variable. + +``` +➜ re --token $REINFER_TOKEN get datasets +``` + +Even better to use contexts, see further below. + +#### Different Clusters + +By default, the endpoint for all commands is `https://reinfer.io`. This can be overidden using `--endpoint`, e.g. + +``` +re --endpoint http://localhost:8000 --token $REINFER_TOKEN get datasets +``` + +#### Contexts (stateful authentication) + +Contexts help avoid having to manually specify the token and endpoint with every command. A _context_ is composed of + +- The authentication token (which user?) +- The reinfer endpoint to talk to, typically `https://reinfer.io` (which cluster?) +- A human rememberable name + +Commands for managing _contexts_ are under `re config` and allow one to create, update, set and delete contexts. Run `re config -h` to see all the options. + +When creating the very first context, this will be set as the active one + +``` +➜ re config add --name production --endpoint https://reinfer.io/ +I A new context `production` will be created. +* Enter API token [none]: MYSUPERSECRETTOKEN +W Be careful, API tokens are stored in cleartext in /home/marius/.config/reinfer/contexts.json. +I New context `production` was created. +``` + +The token and endpoint for the current context will be used for all subsequent commands (these be overwritten as a one off using the `--token` and `--endpoint` arguments). + +``` +➜ re get datasets + Name ID Updated (UTC) Title + InvestmentBank/collateral-triggers aa9dda7c059e5a8d 2019-04-30 17:25:03 IB Collateral Triggers + InvestmentBank/george-test 1aaeacd49dfce8a0 2019-05-10 15:32:34 Test Dataset + InvestmentBank/margin-call b9d50fb2b38c3af5 2019-05-08 07:51:09 IB Margin Call + InvestmentBank/margin-call-large 6d00b9f69ab059f6 2019-05-11 09:23:43 IB Margin Call Large +``` + +### Uploading Comments + +WIP + +## Roadmap and known issues + +- [x] Ability to upload comments to a source +- [x] Ability to upload comments to a source and jointly upload associated + labellings/entities to a dataset (similar to the `tools/put_comments`) +- [ ] Get CI to build `deb` package and binary automatically +- [ ] `inspect` command for resources with a detailed view +- [ ] Configurable columns for table view for `re get` +- [ ] Ability to create users +- [ ] Update operations for sources, datasets and users +- [ ] Specialise errors for common failures (such as missing sources, datasets etc.) +- [ ] Global `--no-color` argument for headless usage +- [ ] CRUD operations for labellers + +### Updating binary and Debian package + +Creating binaries and Debian packages should eventually be done automatically by CI. For now, there's a small script `publish-binaries` that does it. diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 00000000..53eaa219 --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 00000000..24cb73e6 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "reinfer-client" +version = "0.2.0" +authors = ["reinfer Ltd. "] +edition = "2018" + +[lib] +name = "reinfer_client" + +[dependencies] +chrono = { version = "0.4.11", features = ["serde"] } +failchain = "0.1015.2" +failure = "0.1.6" +failure_derive = "0.1.6" +lazy_static = "1.4.0" +log = "0.4.8" +reqwest = { version = "0.10.6", default-features = false, features = ["blocking", "gzip", "json", "native-tls-vendored"] } +serde = { version = "1.0.114", features = ["derive"] } +serde_json = "1.0.55" diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000..f351e194 --- /dev/null +++ b/api/README.md @@ -0,0 +1,3 @@ +# API Library for reinfer + +A Rust library for the reinfer API. diff --git a/api/src/errors.rs b/api/src/errors.rs new file mode 100644 index 00000000..655e70b0 --- /dev/null +++ b/api/src/errors.rs @@ -0,0 +1,77 @@ +use failchain::{BoxedError, ChainErrorKind}; +use failure::Fail; +use reqwest::StatusCode; +use std::result::Result as StdResult; + +pub type Error = BoxedError; +pub type Result = StdResult; + +#[derive(Clone, Eq, PartialEq, Debug, Fail)] +pub enum ErrorKind { + #[fail(display = "API request failed with {}: {}", status_code, message)] + Api { + status_code: StatusCode, + message: String, + }, + + #[fail(display = "Invalid endpoint `{}`", name)] + BadEndpoint { name: String }, + + #[fail(display = "Bad token: {}", token)] + BadToken { token: String }, + + #[fail( + display = "Expected / or a source id, got: {}", + identifier + )] + BadSourceIdentifier { identifier: String }, + + #[fail( + display = "Expected / or a dataset id, got: {}", + identifier + )] + BadDatasetIdentifier { identifier: String }, + + #[fail(display = "Expected //: {}", identifier)] + BadTriggerName { identifier: String }, + + #[fail(display = "Expected a username or user id, got: {}", identifier)] + BadUserIdentifier { identifier: String }, + + #[fail(display = "Unknown organisation permission: {}", permission)] + BadOrganisationPermission { permission: String }, + + #[fail(display = "Unknown global permission: {}", permission)] + BadGlobalPermission { permission: String }, + + #[fail( + display = "Expected / or a bucket id, got: {}", + identifier + )] + BadBucketIdentifier { identifier: String }, + + #[fail(display = "Expected a valid bucket type, got: {}", bucket_type)] + BadBucketType { bucket_type: String }, + + #[fail(display = "Could not parse JSON response.")] + BadJsonResponse, + + #[fail( + display = "Status code {} inconsistent with response payload: {}", + status_code, message + )] + BadProtocol { + status_code: StatusCode, + message: String, + }, + + #[fail(display = "Failed to initialise the HTTP client")] + BuildHttpClient, + + #[fail(display = "An unknown error has occurred: {}", message)] + Unknown { message: String }, +} + +impl ChainErrorKind for ErrorKind { + type Error = Error; +} diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 00000000..b3702d76 --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,1031 @@ +#![deny(clippy::all)] +pub mod errors; +pub mod resources; + +use chrono::{DateTime, Utc}; +use failchain::ResultExt; +use lazy_static::lazy_static; +use log::debug; +use reqwest::{ + blocking::Client as HttpClient, + header::{HeaderMap, HeaderName, HeaderValue}, + IntoUrl, Url, +}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::fmt::Display; + +use crate::resources::{ + bucket::{ + CreateRequest as CreateBucketRequest, CreateResponse as CreateBucketResponse, + GetAvailableResponse as GetAvailableBucketsResponse, GetResponse as GetBucketResponse, + }, + comment::{ + GetAnnotationsResponse, GetLabellingsAfter, GetRecentRequest, RecentCommentsPage, + SyncCommentsRequest, UpdateAnnotationsRequest, + }, + dataset::{ + CreateRequest as CreateDatasetRequest, CreateResponse as CreateDatasetResponse, + GetAvailableResponse as GetAvailableDatasetsResponse, GetResponse as GetDatasetResponse, + }, + email::{PutEmailsRequest, PutEmailsResponse}, + source::{ + CreateRequest as CreateSourceRequest, CreateResponse as CreateSourceResponse, + GetAvailableResponse as GetAvailableSourcesResponse, GetResponse as GetSourceResponse, + }, + statistics::GetResponse as GetStatisticsResponse, + trigger::{ + AdvanceRequest as TriggerAdvanceRequest, FetchRequest as TriggerFetchRequest, + GetResponse as GetTriggersResponse, ResetRequest as TriggerResetRequest, + }, + user::{ + CreateRequest as CreateUserRequest, CreateResponse as CreateUserResponse, + GetAvailableResponse as GetAvailableUsersResponse, + GetCurrentResponse as GetCurrentUserResponse, + }, + ApiError, EmptySuccess, Response, SimpleApiError, +}; + +pub use crate::{ + errors::{Error, ErrorKind, Result}, + resources::{ + bucket::{ + Bucket, BucketType, FullName as BucketFullName, Id as BucketId, + Identifier as BucketIdentifier, Name as BucketName, NewBucket, + }, + comment::{ + AnnotatedComment, Comment, CommentFilter, CommentsIterPage, Continuation, Entity, + Id as CommentId, Label, LabelName, Message, MessageBody, MessageSignature, + MessageSubject, NewAnnotatedComment, NewComment, NewEntities, NewLabelling, + PropertyMap, PropertyValue, Sentiment, SyncCommentsResponse, Uid as CommentUid, + }, + dataset::{ + Dataset, FullName as DatasetFullName, Id as DatasetId, Identifier as DatasetIdentifier, + Name as DatasetName, NewDataset, + }, + email::{Id as EmailId, Mailbox, MimeContent, NewEmail}, + source::{ + FullName as SourceFullName, Id as SourceId, Identifier as SourceIdentifier, + Name as SourceName, NewSource, Source, + }, + statistics::Statistics, + trigger::{ + Batch as TriggerBatch, FullName as TriggerFullName, SequenceId as TriggerSequenceId, + Trigger, + }, + user::{ + Email, GlobalPermission, Id as UserId, Identifier as UserIdentifier, + ModifiedPermissions, NewUser, Organisation, OrganisationPermission, User, Username, + }, + }, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Token(pub String); + +pub struct Config { + pub endpoint: Url, + pub token: Token, + pub accept_invalid_certificates: bool, +} + +impl Default for Config { + fn default() -> Self { + Config { + endpoint: DEFAULT_ENDPOINT.clone(), + token: Token("".to_owned()), + accept_invalid_certificates: false, + } + } +} + +#[derive(Debug)] +pub struct Client { + endpoints: Endpoints, + http_client: HttpClient, + headers: HeaderMap, +} + +#[derive(Serialize)] +pub struct GetLabellingsInBulk<'a> { + pub source_id: &'a SourceId, + pub return_predictions: &'a bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub after: &'a Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: &'a Option, +} + +#[derive(Serialize)] +pub struct GetCommentsIterPageQuery<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + pub from_timestamp: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub to_timestamp: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option<&'a Continuation>, + pub limit: usize, +} + +impl Client { + /// Create a new API client. + pub fn new(config: Config) -> Result { + let http_client = build_http_client(&config)?; + let headers = build_headers(&config)?; + let endpoints = Endpoints::new(config.endpoint)?; + Ok(Client { + endpoints, + http_client, + headers, + }) + } + + /// List all visible sources. + pub fn get_sources(&self) -> Result> { + Ok(self + .get::<_, GetAvailableSourcesResponse, SimpleApiError>(self.endpoints.sources.clone())? + .sources) + } + + /// Get a source by either id or name. + pub fn get_source(&self, source: impl Into) -> Result { + Ok(match source.into() { + SourceIdentifier::Id(source_id) => { + self.get::<_, GetSourceResponse, SimpleApiError>( + self.endpoints.source_by_id(&source_id)?, + )? + .source + } + SourceIdentifier::FullName(source_name) => { + self.get::<_, GetSourceResponse, SimpleApiError>( + self.endpoints.source_by_name(&source_name)?, + )? + .source + } + }) + } + + /// Create a new source. + pub fn create_source( + &self, + source_name: &SourceFullName, + options: NewSource, + ) -> Result { + Ok(self + .put::<_, _, CreateSourceResponse, SimpleApiError>( + self.endpoints.source_by_name(&source_name)?, + CreateSourceRequest { source: options }, + )? + .source) + } + + /// Delete a new source. + pub fn delete_source(&self, source: impl Into + Clone) -> Result<()> { + let source_id = match source.clone().into() { + SourceIdentifier::Id(source_id) => source_id, + SourceIdentifier::FullName(_) => self.get_source(source)?.id, + }; + self.delete::<_, SimpleApiError>(self.endpoints.source_by_id(&source_id)?) + } + + /// Get a page of comments from a source. + pub fn get_comments_iter_page( + &self, + source_name: &SourceFullName, + continuation: Option<&ContinuationKind>, + to_timestamp: Option>, + limit: usize, + ) -> Result { + // Comments are returned from the API in increasing order of their + // `timestamp` field. + let (from_timestamp, after) = match continuation { + // If we have a timestamp, then this is a request for the first page of + // a series of comments with timestamps starting from the given time. + Some(ContinuationKind::Timestamp(from_timestamp)) => (Some(*from_timestamp), None), + // If we have a continuation, then this is a request for page n+1 of + // a series of comments, where the continuation came from page n. + Some(ContinuationKind::Continuation(after)) => (None, Some(after)), + // Otherwise, this is a request for the first page of a series of comments + // with timestamps starting from the beginning of time. + None => (None, None), + }; + let query_params = GetCommentsIterPageQuery { + from_timestamp, + to_timestamp, + limit, + after, + }; + Ok(self.get_query::<_, _, _, SimpleApiError>( + self.endpoints.get_comments(source_name)?, + &query_params, + )?) + } + + /// Iterate through all comments in a source. + pub fn get_comments_iter<'a>( + &'a self, + source_name: &'a SourceFullName, + page_size: Option, + timerange: CommentsIterTimerange, + ) -> CommentsIter<'a> { + CommentsIter::new(self, source_name, page_size, timerange) + } + + pub fn sync_comments( + &self, + source_name: &SourceFullName, + comments: &[NewComment], + ) -> Result { + Ok(self.post::<_, _, _, SimpleApiError>( + self.endpoints.sync_comments(source_name)?, + SyncCommentsRequest { comments }, + )?) + } + + pub fn put_emails( + &self, + bucket_name: &BucketFullName, + emails: &[NewEmail], + ) -> Result { + Ok(self.put::<_, _, _, SimpleApiError>( + self.endpoints.put_emails(bucket_name)?, + PutEmailsRequest { emails }, + )?) + } + + pub fn get_datasets(&self) -> Result> { + Ok(self + .get::<_, GetAvailableDatasetsResponse, SimpleApiError>( + self.endpoints.datasets.clone(), + )? + .datasets) + } + + pub fn get_dataset(&self, dataset: IdentifierT) -> Result + where + IdentifierT: Into, + { + Ok(match dataset.into() { + DatasetIdentifier::Id(dataset_id) => { + self.get::<_, GetDatasetResponse, SimpleApiError>( + self.endpoints.dataset_by_id(&dataset_id)?, + )? + .dataset + } + DatasetIdentifier::FullName(dataset_name) => { + self.get::<_, GetDatasetResponse, SimpleApiError>( + self.endpoints.dataset_by_name(&dataset_name)?, + )? + .dataset + } + }) + } + + pub fn create_dataset( + &self, + dataset_name: &DatasetFullName, + options: NewDataset, + ) -> Result { + Ok(self + .put::<_, _, CreateDatasetResponse, SimpleApiError>( + self.endpoints.dataset_by_name(dataset_name)?, + CreateDatasetRequest { dataset: options }, + )? + .dataset) + } + + pub fn delete_dataset(&self, dataset: IdentifierT) -> Result<()> + where + IdentifierT: Into + Clone, + { + let dataset_id = match dataset.clone().into() { + DatasetIdentifier::Id(dataset_id) => dataset_id, + DatasetIdentifier::FullName(_) => self.get_dataset(dataset)?.id, + }; + self.delete::<_, SimpleApiError>(self.endpoints.dataset_by_id(&dataset_id)?) + } + + /// Get labellings for a given a dataset and a list of comment UIDs. + pub fn get_labellings<'a>( + &self, + dataset_name: &DatasetFullName, + comment_uids: impl Iterator, + ) -> Result> { + Ok(self + .get_query::<_, _, GetAnnotationsResponse, SimpleApiError>( + self.endpoints.get_labellings(dataset_name)?, + &[("ids", comment_uids_comma_separated_list(comment_uids))], + )? + .results) + } + + /// Iterate through all reviewed comments in a source. + pub fn get_labellings_iter<'a>( + &'a self, + dataset_name: &'a DatasetFullName, + source_id: &'a SourceId, + return_predictions: bool, + limit: Option, + ) -> LabellingsIter<'a> { + LabellingsIter::new(self, dataset_name, source_id, return_predictions, limit) + } + + /// Get reviewed comments in bulk + pub fn get_labellings_in_bulk( + &self, + dataset_name: &DatasetFullName, + query_parameters: GetLabellingsInBulk, + ) -> Result { + Ok( + self.get_query::<_, _, GetAnnotationsResponse, SimpleApiError>( + self.endpoints.get_labellings(dataset_name)?, + &query_parameters, + )?, + ) + } + + /// Update labellings for a given a dataset and comment UID. + pub fn update_labelling( + &self, + dataset_name: &DatasetFullName, + comment_uid: &CommentUid, + labelling: Option<&NewLabelling>, + entities: Option<&NewEntities>, + ) -> Result { + Ok(self.post::<_, _, AnnotatedComment, SimpleApiError>( + self.endpoints.post_labelling(dataset_name, comment_uid)?, + UpdateAnnotationsRequest { + labelling, + entities, + }, + )?) + } + + pub fn get_triggers(&self, dataset_name: &DatasetFullName) -> Result> { + Ok(self + .get::<_, GetTriggersResponse, SimpleApiError>(self.endpoints.triggers(dataset_name)?)? + .triggers) + } + + pub fn get_recent_comments( + &self, + dataset_name: &DatasetFullName, + filter: &CommentFilter, + limit: usize, + continuation: Option<&Continuation>, + ) -> Result { + Ok(self.post::<_, _, RecentCommentsPage, SimpleApiError>( + self.endpoints.recent_comments(dataset_name)?, + GetRecentRequest { + limit, + filter, + continuation, + }, + )?) + } + + pub fn get_current_user(&self) -> Result { + Ok(self + .get::<_, GetCurrentUserResponse, SimpleApiError>(self.endpoints.current_user.clone())? + .user) + } + + pub fn get_users(&self) -> Result> { + Ok(self + .get::<_, GetAvailableUsersResponse, SimpleApiError>(self.endpoints.users.clone())? + .users) + } + + pub fn create_user(&self, user: NewUser) -> Result { + Ok(self + .put::<_, _, CreateUserResponse, SimpleApiError>( + self.endpoints.users.clone(), + CreateUserRequest { user }, + )? + .user) + } + + pub fn get_statistics(&self, dataset_name: &DatasetFullName) -> Result { + Ok(self + .post::<_, _, GetStatisticsResponse, SimpleApiError>( + self.endpoints.statistics(dataset_name)?, + json!({}), + )? + .statistics) + } + + /// Create a new bucket. + pub fn create_bucket( + &self, + bucket_name: &BucketFullName, + options: NewBucket, + ) -> Result { + Ok(self + .put::<_, _, CreateBucketResponse, SimpleApiError>( + self.endpoints.bucket_by_name(&bucket_name)?, + CreateBucketRequest { bucket: options }, + )? + .bucket) + } + + pub fn get_buckets(&self) -> Result> { + Ok(self + .get::<_, GetAvailableBucketsResponse, SimpleApiError>(self.endpoints.buckets.clone())? + .buckets) + } + + pub fn get_bucket(&self, bucket: IdentifierT) -> Result + where + IdentifierT: Into, + { + Ok(match bucket.into() { + BucketIdentifier::Id(bucket_id) => { + self.get::<_, GetBucketResponse, SimpleApiError>( + self.endpoints.bucket_by_id(&bucket_id)?, + )? + .bucket + } + BucketIdentifier::FullName(bucket_name) => { + self.get::<_, GetBucketResponse, SimpleApiError>( + self.endpoints.bucket_by_name(&bucket_name)?, + )? + .bucket + } + }) + } + + pub fn delete_bucket(&self, bucket: IdentifierT) -> Result<()> + where + IdentifierT: Into + Clone, + { + let bucket_id = match bucket.clone().into() { + BucketIdentifier::Id(bucket_id) => bucket_id, + BucketIdentifier::FullName(_) => self.get_bucket(bucket)?.id, + }; + self.delete::<_, SimpleApiError>(self.endpoints.bucket_by_id(&bucket_id)?) + } + + pub fn fetch_trigger_comments( + &self, + trigger_name: &TriggerFullName, + size: u32, + ) -> Result { + self.post::<_, _, _, SimpleApiError>( + self.endpoints.trigger_fetch(trigger_name)?, + TriggerFetchRequest { size }, + ) + } + + pub fn advance_trigger( + &self, + trigger_name: &TriggerFullName, + sequence_id: TriggerSequenceId, + ) -> Result<()> { + self.post::<_, _, serde::de::IgnoredAny, SimpleApiError>( + self.endpoints.trigger_advance(trigger_name)?, + TriggerAdvanceRequest { sequence_id }, + )?; + Ok(()) + } + + pub fn reset_trigger( + &self, + trigger_name: &TriggerFullName, + to_comment_created_at: DateTime, + ) -> Result<()> { + self.post::<_, _, serde::de::IgnoredAny, SimpleApiError>( + self.endpoints.trigger_reset(trigger_name)?, + TriggerResetRequest { + to_comment_created_at, + }, + )?; + Ok(()) + } + + fn get(&self, url: LocationT) -> Result + where + LocationT: IntoUrl + Display, + for<'de> SuccessT: Deserialize<'de>, + for<'de> ErrorT: Deserialize<'de> + ApiError, + { + debug!("Attempting GET `{}`", url); + let http_response = self + .http_client + .get(url) + .headers(self.headers.clone()) + .send() + .chain_err(|| ErrorKind::Unknown { + message: "GET operation failed.".to_owned(), + })?; + let status = http_response.status(); + http_response + .json::>() + .chain_err(|| ErrorKind::BadJsonResponse)? + .into_result(status) + } + + fn get_query( + &self, + url: LocationT, + query: &QueryT, + ) -> Result + where + LocationT: IntoUrl + Display, + QueryT: Serialize, + for<'de> SuccessT: Deserialize<'de>, + for<'de> ErrorT: Deserialize<'de> + ApiError, + { + debug!("Attempting GET `{}`", url); + let http_response = self + .http_client + .get(url) + .headers(self.headers.clone()) + .query(query) + .send() + .chain_err(|| ErrorKind::Unknown { + message: "GET operation failed.".to_owned(), + })?; + let status = http_response.status(); + http_response + .json::>() + .chain_err(|| ErrorKind::BadJsonResponse)? + .into_result(status) + } + + fn delete(&self, url: LocationT) -> Result<()> + where + LocationT: IntoUrl + Display, + for<'de> ErrorT: Deserialize<'de> + ApiError, + { + debug!("Attempting DELETE `{}`", url); + let http_response = self + .http_client + .delete(url) + .headers(self.headers.clone()) + .send() + .chain_err(|| ErrorKind::Unknown { + message: "DELETE operation failed.".to_owned(), + })?; + let status = http_response.status(); + http_response + .json::>() + .chain_err(|| ErrorKind::BadJsonResponse)? + .into_result(status) + .map(|_| ()) + } + + fn post( + &self, + url: LocationT, + request: RequestT, + ) -> Result + where + LocationT: IntoUrl + Display, + RequestT: Serialize, + for<'de> SuccessT: Deserialize<'de>, + for<'de> ErrorT: Deserialize<'de> + ApiError, + { + debug!("Attempting POST `{}`", url); + let http_response = self + .http_client + .post(url) + .headers(self.headers.clone()) + .json(&request) + .send() + .chain_err(|| ErrorKind::Unknown { + message: "POST operation failed.".to_owned(), + })?; + let status = http_response.status(); + http_response + .json::>() + .chain_err(|| ErrorKind::BadJsonResponse)? + .into_result(status) + } + + fn put( + &self, + url: LocationT, + request: RequestT, + ) -> Result + where + LocationT: IntoUrl + Display, + RequestT: Serialize, + for<'de> SuccessT: Deserialize<'de>, + for<'de> ErrorT: Deserialize<'de> + ApiError, + { + debug!("Attempting PUT `{}`", url); + let http_response = self + .http_client + .put(url) + .headers(self.headers.clone()) + .json(&request) + .send() + .chain_err(|| ErrorKind::Unknown { + message: "PUT operation failed.".to_owned(), + })?; + let status = http_response.status(); + http_response + .json::>() + .chain_err(|| ErrorKind::BadJsonResponse)? + .into_result(status) + } +} + +pub enum ContinuationKind { + Timestamp(DateTime), + Continuation(Continuation), +} + +pub struct CommentsIter<'a> { + client: &'a Client, + source_name: &'a SourceFullName, + continuation: Option, + done: bool, + page_size: usize, + to_timestamp: Option>, +} + +#[derive(Default)] +pub struct CommentsIterTimerange { + pub from: Option>, + pub to: Option>, +} +impl<'a> CommentsIter<'a> { + // Default number of comments per page to request from API. + const DEFAULT_PAGE_SIZE: usize = 64; + + fn new( + client: &'a Client, + source_name: &'a SourceFullName, + page_size: Option, + timerange: CommentsIterTimerange, + ) -> Self { + let (from_timestamp, to_timestamp) = (timerange.from, timerange.to); + Self { + client, + source_name, + to_timestamp, + continuation: from_timestamp.map(ContinuationKind::Timestamp), + done: false, + page_size: page_size.unwrap_or(Self::DEFAULT_PAGE_SIZE), + } + } +} + +impl<'a> Iterator for CommentsIter<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + let response = self.client.get_comments_iter_page( + self.source_name, + self.continuation.as_ref(), + self.to_timestamp, + self.page_size, + ); + Some(response.map(|page| { + self.continuation = page.continuation.map(ContinuationKind::Continuation); + self.done = self.continuation.is_none(); + page.comments + })) + } +} + +pub struct LabellingsIter<'a> { + client: &'a Client, + dataset_name: &'a DatasetFullName, + source_id: &'a SourceId, + return_predictions: bool, + after: Option, + limit: Option, + done: bool, +} + +impl<'a> LabellingsIter<'a> { + fn new( + client: &'a Client, + dataset_name: &'a DatasetFullName, + source_id: &'a SourceId, + return_predictions: bool, + limit: Option, + ) -> Self { + Self { + client, + dataset_name, + source_id, + return_predictions, + after: None, + limit, + done: false, + } + } +} + +impl<'a> Iterator for LabellingsIter<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + let response = self.client.get_labellings_in_bulk( + self.dataset_name, + GetLabellingsInBulk { + source_id: self.source_id, + return_predictions: &self.return_predictions, + after: &self.after, + limit: &self.limit, + }, + ); + Some(response.map(|page| { + if self.after == page.after && !page.results.is_empty() { + panic!("Labellings API did not increment pagination continuation"); + } + self.after = page.after; + if page.results.is_empty() { + self.done = true; + } + page.results + })) + } +} + +#[derive(Debug)] +struct Endpoints { + base: Url, + datasets: Url, + sources: Url, + buckets: Url, + users: Url, + current_user: Url, +} + +impl Endpoints { + pub fn new(base: Url) -> Result { + let datasets = base + .join("/api/v1/datasets") + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL for dataset resources.".to_owned(), + })?; + let sources = base + .join("/api/v1/sources") + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL for source resources.".to_owned(), + })?; + let buckets = base + .join("/api/_private/buckets") + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL for bucket resources.".to_owned(), + })?; + let users = base + .join("/api/_private/users") + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL for users resources.".to_owned(), + })?; + let current_user = base.join("/auth/user").chain_err(|| ErrorKind::Unknown { + message: "Could not build URL for users resources.".to_owned(), + })?; + Ok(Endpoints { + base, + datasets, + sources, + buckets, + users, + current_user, + }) + } + + fn triggers(&self, dataset_name: &DatasetFullName) -> Result { + self.base + .join(&format!("/api/v1/datasets/{}/triggers", dataset_name.0)) + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL to get trigger resources.".to_owned(), + }) + } + + fn trigger_fetch(&self, trigger_name: &TriggerFullName) -> Result { + self.base + .join(&format!( + "/api/v1/datasets/{}/triggers/{}/fetch", + trigger_name.dataset.0, trigger_name.trigger.0 + )) + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL to fetch trigger results.".to_owned(), + }) + } + + fn trigger_advance(&self, trigger_name: &TriggerFullName) -> Result { + self.base + .join(&format!( + "/api/v1/datasets/{}/triggers/{}/advance", + trigger_name.dataset.0, trigger_name.trigger.0 + )) + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL to advance triggers.".to_owned(), + }) + } + + fn trigger_reset(&self, trigger_name: &TriggerFullName) -> Result { + self.base + .join(&format!( + "/api/v1/datasets/{}/triggers/{}/reset", + trigger_name.dataset.0, trigger_name.trigger.0 + )) + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL to reset triggers.".to_owned(), + }) + } + + fn recent_comments(&self, dataset_name: &DatasetFullName) -> Result { + self.base + .join(&format!("/api/_private/datasets/{}/recent", dataset_name.0)) + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL for recent comments query.".to_owned(), + }) + } + + fn statistics(&self, dataset_name: &DatasetFullName) -> Result { + self.base + .join(&format!( + "/api/_private/datasets/{}/statistics", + dataset_name.0 + )) + .chain_err(|| ErrorKind::Unknown { + message: "Could not build URL for dataset statistics query.".to_owned(), + }) + } + + fn source_by_id(&self, source_id: &SourceId) -> Result { + self.base + .join(&format!("/api/v1/sources/id:{}", source_id.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build URL for source resource with id `{}`.", + source_id.0 + ), + }) + } + + fn source_by_name(&self, source_name: &SourceFullName) -> Result { + self.base + .join(&format!("/api/v1/sources/{}", source_name.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build URL for source resource with name `{}`.", + source_name.0 + ), + }) + } + + fn get_comments(&self, source_name: &SourceFullName) -> Result { + self.base + .join(&format!("/api/_private/sources/{}/comments", source_name.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build get comments URL for source `{}`.", + source_name.0, + ), + }) + } + + fn sync_comments(&self, source_name: &SourceFullName) -> Result { + self.base + .join(&format!("/api/v1/sources/{}/sync", source_name.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!("Could not build sync URL for source `{}`.", source_name.0,), + }) + } + + fn put_emails(&self, bucket_name: &BucketFullName) -> Result { + self.base + .join(&format!("/api/_private/buckets/{}/emails", bucket_name.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build put emails URL for bucket `{}`.", + bucket_name, + ), + }) + } + + fn dataset_by_id(&self, dataset_id: &DatasetId) -> Result { + self.base + .join(&format!("/api/v1/datasets/id:{}", dataset_id.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build URL for dataset resource with id `{}`.", + dataset_id.0 + ), + }) + } + + fn dataset_by_name(&self, dataset_name: &DatasetFullName) -> Result { + self.base + .join(&format!("/api/v1/datasets/{}", dataset_name.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build URL for dataset resource with name `{}`.", + dataset_name.0 + ), + }) + } + + fn get_labellings(&self, dataset_name: &DatasetFullName) -> Result { + self.base + .join(&format!( + "/api/_private/datasets/{}/labellings", + dataset_name.0 + )) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build get labellings URL for dataset `{}`.", + dataset_name.0, + ), + }) + } + + fn post_labelling( + &self, + dataset_name: &DatasetFullName, + comment_uid: &CommentUid, + ) -> Result { + self.base + .join(&format!( + "/api/_private/datasets/{}/labellings/{}", + dataset_name.0, comment_uid.0, + )) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build get labellings URL for dataset `{}`.", + dataset_name.0, + ), + }) + } + + fn bucket_by_id(&self, bucket_id: &BucketId) -> Result { + self.base + .join(&format!("/api/_private/buckets/id:{}", bucket_id.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build URL for bucket resource with id `{}`.", + bucket_id.0 + ), + }) + } + + fn bucket_by_name(&self, bucket_name: &BucketFullName) -> Result { + self.base + .join(&format!("/api/_private/buckets/{}", bucket_name.0)) + .chain_err(|| ErrorKind::Unknown { + message: format!( + "Could not build URL for bucket resource with name `{}`.", + bucket_name.0 + ), + }) + } +} + +fn build_http_client(config: &Config) -> Result { + HttpClient::builder() + .gzip(true) + .danger_accept_invalid_certs(config.accept_invalid_certificates) + .build() + .chain_err(|| ErrorKind::BuildHttpClient) +} + +fn build_headers(config: &Config) -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + AUTH_HEADER_NAME.clone(), + HeaderValue::from_str(&format!("Bearer {}", &config.token.0)).chain_err(|| { + ErrorKind::BadToken { + token: config.token.0.clone(), + } + })?, + ); + Ok(headers) +} + +fn comment_uids_comma_separated_list<'a>( + mut comment_uids: impl Iterator, +) -> String { + // Build `query_uids == ",".join(comment_uids)` + let mut query_uids = String::new(); + if let Some(first_uid) = comment_uids.next() { + query_uids.push_str(&first_uid.0); + for comment_uid in comment_uids { + query_uids.push_str(","); + query_uids.push_str(&comment_uid.0); + } + } + query_uids +} + +lazy_static! { + static ref AUTH_HEADER_NAME: HeaderName = HeaderName::from_static("authorization"); + pub static ref DEFAULT_ENDPOINT: Url = + Url::parse("https://reinfer.io").expect("Default URL is well-formed"); +} diff --git a/api/src/resources/bucket.rs b/api/src/resources/bucket.rs new file mode 100644 index 00000000..14466bd3 --- /dev/null +++ b/api/src/resources/bucket.rs @@ -0,0 +1,155 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::str::FromStr; + +use crate::{ + errors::{Error, ErrorKind, Result}, + resources::user::Username, +}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Bucket { + pub id: Id, + pub name: Name, + pub owner: Username, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Bucket { + pub fn full_name(&self) -> FullName { + FullName(format!("{}/{}", self.owner.0, self.name.0)) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Name(pub String); + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct FullName(pub String); + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Id(pub String); + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ModelFamily(pub String); + +// TODO(mcobzarenco)[3963]: Make `Identifier` into a trait (ensure it still implements +// `FromStr` so we can take T: Identifier as a clap command line argument). +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum Identifier { + Id(Id), + FullName(FullName), +} + +impl From for Identifier { + fn from(full_name: FullName) -> Self { + Identifier::FullName(full_name) + } +} + +impl From for Identifier { + fn from(id: Id) -> Self { + Identifier::Id(id) + } +} + +impl FromStr for Identifier { + type Err = Error; + + fn from_str(string: &str) -> Result { + if string.chars().all(|c| c.is_digit(16)) { + Ok(Identifier::Id(Id(string.into()))) + } else if string.split('/').count() == 2 { + Ok(Identifier::FullName(FullName(string.into()))) + } else { + Err(ErrorKind::BadBucketIdentifier { + identifier: string.into(), + } + .into()) + } + } +} + +impl Display for FullName { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "{}", self.0) + } +} + +impl Display for Id { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "{}", self.0) + } +} + +impl Display for Identifier { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + match *self { + Identifier::Id(ref id) => Display::fmt(id, formatter), + Identifier::FullName(ref full_name) => Display::fmt(full_name, formatter), + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct NewBucket<'request> { + pub bucket_type: BucketType, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option<&'request str>, +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct CreateRequest<'request> { + pub bucket: NewBucket<'request>, +} + +#[derive(Debug, Clone, Deserialize)] +pub(crate) struct CreateResponse { + pub bucket: Bucket, +} + +#[derive(Debug, Clone, Deserialize)] +pub(crate) struct GetAvailableResponse { + pub buckets: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub(crate) struct GetResponse { + pub bucket: Bucket, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum BucketType { + #[serde(rename = "emails")] + Emails, +} + +impl FromStr for BucketType { + type Err = Error; + + fn from_str(string: &str) -> Result { + match string { + "emails" => Ok(Self::Emails), + _ => Err(ErrorKind::BadBucketType { + bucket_type: string.into(), + } + .into()), + } + } +} + +impl Default for BucketType { + fn default() -> Self { + Self::Emails + } +} + +impl Display for BucketType { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + match *self { + Self::Emails => write!(formatter, "emails"), + } + } +} diff --git a/api/src/resources/comment.rs b/api/src/resources/comment.rs new file mode 100644 index 00000000..527d44e8 --- /dev/null +++ b/api/src/resources/comment.rs @@ -0,0 +1,644 @@ +use crate::errors::Error; +use chrono::{DateTime, Utc}; +use serde::{ + de::{Deserializer, Error as SerdeError, MapAccess, Visitor}, + ser::{SerializeMap, Serializer}, + Deserialize, Serialize, +}; +use serde_json::{json, Value as JsonValue}; +use std::str::FromStr; +use std::{ + collections::HashMap, + fmt::{Formatter, Result as FmtResult}, + ops::{Deref, DerefMut}, + result::Result as StdResult, +}; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub struct Id(pub String); + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub struct Uid(pub String); + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub struct ThreadId(pub String); + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct CommentFilter(pub JsonValue); + +impl Default for CommentFilter { + fn default() -> Self { + Self::empty() + } +} + +impl CommentFilter { + pub fn empty() -> Self { + CommentFilter(json!({})) + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct GetRecentRequest<'a> { + pub limit: usize, + pub filter: &'a CommentFilter, + #[serde(skip_serializing_if = "Option::is_none")] + pub continuation: Option<&'a Continuation>, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub struct Continuation(pub String); + +#[derive(Debug, Clone, Deserialize)] +pub struct RecentCommentsPage { + pub results: Vec, + pub continuation: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub struct GetLabellingsAfter(pub String); + +#[derive(Debug, Clone, Deserialize)] +pub struct GetAnnotationsResponse { + pub results: Vec, + #[serde(default)] + pub after: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct UpdateAnnotationsRequest<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + pub labelling: Option<&'a NewLabelling>, + #[serde(skip_serializing_if = "Option::is_none")] + pub entities: Option<&'a NewEntities>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CommentsIterPage { + pub comments: Vec, + pub continuation: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct SyncCommentsRequest<'request> { + pub comments: &'request [NewComment], +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SyncCommentsResponse { + pub new: usize, + pub updated: usize, + pub unchanged: usize, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct Comment { + pub id: Id, + pub uid: Uid, + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + pub timestamp: DateTime, + pub messages: Vec, + #[serde(skip_serializing_if = "PropertyMap::is_empty", default)] + pub user_properties: PropertyMap, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct NewComment { + pub id: Id, + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + pub timestamp: DateTime, + pub messages: Vec, + #[serde(skip_serializing_if = "PropertyMap::is_empty", default)] + pub user_properties: PropertyMap, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct Message { + pub body: MessageBody, + + #[serde(skip_serializing_if = "Option::is_none")] + pub language: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub signature: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub cc: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub bcc: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub sent_at: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct MessageBody { + pub text: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub translated_from: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct MessageSubject { + pub text: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub translated_from: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct MessageSignature { + pub text: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub translated_from: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub enum Sentiment { + #[serde(rename = "positive")] + Positive, + + #[serde(rename = "negative")] + Negative, +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct PropertyMap(HashMap); + +#[derive(Clone, Debug, PartialEq)] +pub enum PropertyValue { + String(String), + Number(f64), +} + +impl Deref for PropertyMap { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for PropertyMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl PropertyMap { + #[inline] + pub fn new() -> Self { + Default::default() + } + + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + PropertyMap(HashMap::with_capacity(capacity)) + } + + #[inline] + pub fn insert_number(&mut self, key: String, value: f64) { + self.0.insert(key, PropertyValue::Number(value)); + } + + #[inline] + pub fn insert_string(&mut self, key: String, value: String) { + self.0.insert(key, PropertyValue::String(value)); + } + + // Provided despite deref, for `skip_serializing_if`. + #[inline] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +const STRING_PROPERTY_PREFIX: &str = "string:"; +const NUMBER_PROPERTY_PREFIX: &str = "number:"; + +impl Serialize for PropertyMap { + fn serialize(&self, serializer: S) -> Result { + let mut state = serializer.serialize_map(Some(self.len()))?; + if self.0.is_empty() { + return state.end(); + } + + let mut full_name = String::with_capacity(32); + for (key, value) in &self.0 { + full_name.clear(); + match *value { + PropertyValue::String(ref value) => { + if !value.trim().is_empty() { + full_name.push_str(STRING_PROPERTY_PREFIX); + full_name.push_str(key); + state.serialize_entry(&full_name, &value)?; + } + } + PropertyValue::Number(value) => { + full_name.push_str(NUMBER_PROPERTY_PREFIX); + full_name.push_str(key); + state.serialize_entry(&full_name, &value)?; + } + } + } + state.end() + } +} + +impl<'de> Deserialize<'de> for PropertyMap { + #[inline] + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_any(PropertyMapVisitor) + } +} + +struct PropertyMapVisitor; +impl<'de> Visitor<'de> for PropertyMapVisitor { + type Value = PropertyMap; + + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "a user property map") + } + + #[inline] + fn visit_unit(self) -> Result { + Ok(PropertyMap::new()) + } + + fn visit_map(self, mut access: M) -> StdResult + where + M: MapAccess<'de>, + { + let mut values = PropertyMap::with_capacity(access.size_hint().unwrap_or(0)); + + while let Some(mut key) = access.next_key()? { + if strip_prefix(&mut key, STRING_PROPERTY_PREFIX) { + values.insert(key, PropertyValue::String(access.next_value()?)); + } else if strip_prefix(&mut key, NUMBER_PROPERTY_PREFIX) { + values.insert(key, PropertyValue::Number(access.next_value()?)); + } else { + return Err(M::Error::custom(format!( + "user property full name `{}` has invalid \ + type prefix", + key + ))); + } + } + + Ok(values) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AnnotatedComment { + pub comment: Comment, + #[serde(skip_serializing_if = "should_skip_serializing_labelling")] + pub labelling: Option, + #[serde(skip_serializing_if = "should_skip_serializing_entities")] + pub entities: Option, +} + +impl AnnotatedComment { + pub fn is_reviewed(&self) -> bool { + let has_labels = self + .labelling + .as_ref() + .map(|labelling| !labelling.assigned.is_empty() || !labelling.dismissed.is_empty()) + .unwrap_or(false); + let has_entities = self + .entities + .as_ref() + .map(|entities| !entities.assigned.is_empty() || !entities.dismissed.is_empty()) + .unwrap_or(false); + has_labels || has_entities + } + + pub fn without_predictions(mut self) -> Self { + self.labelling = self.labelling.and_then(|mut labelling| { + if labelling.assigned.is_empty() && labelling.dismissed.is_empty() { + None + } else { + labelling.predicted = None; + Some(labelling) + } + }); + self.entities = self.entities.and_then(|mut entities| { + if entities.assigned.is_empty() && entities.dismissed.is_empty() { + None + } else { + entities.predicted = None; + Some(entities) + } + }); + self + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct NewAnnotatedComment { + pub comment: NewComment, + #[serde(skip_serializing_if = "Option::is_none")] + pub labelling: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub entities: Option, +} + +impl NewAnnotatedComment { + pub fn is_reviewed(&self) -> bool { + let has_labels = self + .labelling + .as_ref() + .map(|labelling| !labelling.assigned.is_empty() || !labelling.dismissed.is_empty()) + .unwrap_or(false); + let has_entities = self + .entities + .as_ref() + .map(|entities| !entities.assigned.is_empty() || !entities.dismissed.is_empty()) + .unwrap_or(false); + has_labels || has_entities + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Labelling { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub assigned: Vec