Repository: rigetti/qcs-sdk-rust (language: python)

Issue:
feat: support protoquil flag in compilation
This change makes it possible to configure the compiler such that it produces "protoquil" as its output.

BREAKING: Adds arguments to some functions.
feat: support protoquil flag in compilation
This change makes it possible to configure the compiler such that it produces "protoquil" as its output.

BREAKING: Adds arguments to some functions.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 4c9bc5cdc..27067dc22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,7 @@ qcs-api/docs
 
 # macOS
 .DS_Store
+
+# Python artifacts
+**/__pycache__
+crates/python/qcs_sdk/*.so
diff --git a/Cargo.lock b/Cargo.lock
index a6d4e0aa8..e3650c80f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,11 +2,20 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "anyhow"
-version = "1.0.65"
+version = "1.0.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
+checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
 
 [[package]]
 name = "async-stream"
@@ -31,9 +40,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.57"
+version = "0.1.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
+checksum = "eff18d764974428cf3a9328e23fc5c986f5fbed46e6cd4cdf42544df5d297ec1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -46,7 +55,7 @@ version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
  "winapi",
 ]
@@ -59,9 +68,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "axum"
-version = "0.5.16"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043"
+checksum = "678c5130a507ae3a7c797f9a17393c14849300b8440eac47cdb90a5bdcb3a543"
 dependencies = [
  "async-trait",
  "axum-core",
@@ -77,9 +86,9 @@ dependencies = [
  "mime",
  "percent-encoding",
  "pin-project-lite",
+ "rustversion",
  "serde",
  "sync_wrapper",
- "tokio",
  "tower",
  "tower-http",
  "tower-layer",
@@ -88,9 +97,9 @@ dependencies = [
 
 [[package]]
 name = "axum-core"
-version = "0.2.8"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b"
+checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34"
 dependencies = [
  "async-trait",
  "bytes",
@@ -98,15 +107,22 @@ dependencies = [
  "http",
  "http-body",
  "mime",
+ "rustversion",
  "tower-layer",
  "tower-service",
 ]
 
 [[package]]
 name = "base64"
-version = "0.13.0"
+version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
 
 [[package]]
 name = "bitflags"
@@ -123,21 +139,11 @@ dependencies = [
  "generic-array",
 ]
 
-[[package]]
-name = "buf_redux"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
-dependencies = [
- "memchr",
- "safemem",
-]
-
 [[package]]
 name = "bumpalo"
-version = "3.11.0"
+version = "3.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
 
 [[package]]
 name = "bytecount"
@@ -153,15 +159,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "bytes"
-version = "1.2.1"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
+checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
 
 [[package]]
 name = "cc"
-version = "1.0.73"
+version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
 
 [[package]]
 name = "cfg-if"
@@ -171,9 +177,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "cmake"
-version = "0.1.48"
+version = "0.1.49"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a"
+checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c"
 dependencies = [
  "cc",
 ]
@@ -213,11 +219,21 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "digest"
-version = "0.10.5"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
 dependencies = [
  "block-buffer",
  "crypto-common",
@@ -272,9 +288,9 @@ dependencies = [
 
 [[package]]
 name = "erased-serde"
-version = "0.3.23"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54558e0ba96fbe24280072642eceb9d7d442e32c7ec0ea9e7ecd7b4ea2cf4e11"
+checksum = "e4ca605381c017ec7a5fef5e548f1cfaa419ed0f6df6367339300db74c92aa7d"
 dependencies = [
  "serde",
 ]
@@ -326,9 +342,9 @@ dependencies = [
 
 [[package]]
 name = "futures"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
+checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -341,9 +357,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
+checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -351,15 +367,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
+checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
+checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -368,15 +384,15 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
+checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
 
 [[package]]
 name = "futures-macro"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
+checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -385,21 +401,21 @@ dependencies = [
 
 [[package]]
 name = "futures-sink"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
+checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
 
 [[package]]
 name = "futures-task"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
+checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
 
 [[package]]
 name = "futures-util"
-version = "0.3.24"
+version = "0.3.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
+checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -425,20 +441,31 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.7"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
 dependencies = [
  "cfg-if",
  "libc",
  "wasi",
 ]
 
+[[package]]
+name = "ghost"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41973d4c45f7a35af8753ba3457cc99d406d863941fd7f52663cff54a5ab99b3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "h2"
-version = "0.3.14"
+version = "0.3.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
+checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
 dependencies = [
  "bytes",
  "fnv",
@@ -465,7 +492,7 @@ version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
 dependencies = [
- "base64",
+ "base64 0.13.1",
  "bitflags",
  "bytes",
  "headers-core",
@@ -499,6 +526,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -547,9 +583,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
 
 [[package]]
 name = "hyper"
-version = "0.14.20"
+version = "0.14.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
+checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -571,9 +607,9 @@ dependencies = [
 
 [[package]]
 name = "hyper-rustls"
-version = "0.23.0"
+version = "0.23.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
+checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
 dependencies = [
  "http",
  "hyper",
@@ -606,9 +642,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "1.9.1"
+version = "1.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
 dependencies = [
  "autocfg",
  "hashbrown",
@@ -616,9 +652,9 @@ dependencies = [
 
 [[package]]
 name = "indoc"
-version = "1.0.7"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
+checksum = "da2d6f23ffea9d7e76c53eee25dfb67bcd8fde7f1198b0855350698c9f07c780"
 
 [[package]]
 name = "instant"
@@ -629,11 +665,21 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "inventory"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16fe3b35d64bd1f72917f06425e7573a2f63f74f42c8f56e53ea6826dde3a2b5"
+dependencies = [
+ "ctor",
+ "ghost",
+]
+
 [[package]]
 name = "ipnet"
-version = "2.5.0"
+version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
+checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
 
 [[package]]
 name = "itertools"
@@ -646,9 +692,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.4"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
 
 [[package]]
 name = "js-sys"
@@ -661,11 +707,11 @@ dependencies = [
 
 [[package]]
 name = "jsonwebtoken"
-version = "8.1.1"
+version = "8.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c"
+checksum = "09f4f04699947111ec1733e71778d763555737579e44b85844cae8e1940a1828"
 dependencies = [
- "base64",
+ "base64 0.13.1",
  "pem",
  "ring",
  "serde",
@@ -754,9 +800,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.135"
+version = "0.2.139"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
 
 [[package]]
 name = "lock_api"
@@ -785,9 +831,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
 
 [[package]]
 name = "matchit"
-version = "0.5.0"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
+checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
 
 [[package]]
 name = "memchr"
@@ -839,9 +885,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "mio"
-version = "0.8.4"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
+checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
 dependencies = [
  "libc",
  "log",
@@ -855,29 +901,11 @@ version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
 
-[[package]]
-name = "multipart"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
-dependencies = [
- "buf_redux",
- "httparse",
- "log",
- "mime",
- "mime_guess",
- "quick-error",
- "rand",
- "safemem",
- "tempfile",
- "twoway",
-]
-
 [[package]]
 name = "nom"
-version = "7.1.1"
+version = "7.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
 dependencies = [
  "memchr",
  "minimal-lexical",
@@ -885,9 +913,9 @@ dependencies = [
 
 [[package]]
 name = "nom_locate"
-version = "4.0.0"
+version = "4.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605"
+checksum = "b1e299bf5ea7b212e811e71174c5d1a5d065c4c0ad0c8691ecb1f97e3e66025e"
 dependencies = [
  "bytecount",
  "memchr",
@@ -917,15 +945,17 @@ dependencies = [
  "autocfg",
  "num-integer",
  "num-traits",
+ "serde",
 ]
 
 [[package]]
 name = "num-complex"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
+checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
 dependencies = [
  "num-traits",
+ "serde",
 ]
 
 [[package]]
@@ -959,6 +989,7 @@ dependencies = [
  "num-bigint",
  "num-integer",
  "num-traits",
+ "serde",
 ]
 
 [[package]]
@@ -972,28 +1003,19 @@ dependencies = [
 
 [[package]]
 name = "num_cpus"
-version = "1.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
-dependencies = [
- "hermit-abi",
- "libc",
-]
-
-[[package]]
-name = "num_threads"
-version = "0.1.6"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
 dependencies = [
+ "hermit-abi 0.2.6",
  "libc",
 ]
 
 [[package]]
 name = "once_cell"
-version = "1.15.0"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
 
 [[package]]
 name = "openssl-probe"
@@ -1013,9 +1035,9 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.3"
+version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
 dependencies = [
  "cfg-if",
  "libc",
@@ -1026,9 +1048,9 @@ dependencies = [
 
 [[package]]
 name = "paste"
-version = "1.0.9"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
+checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
 
 [[package]]
 name = "pbjson"
@@ -1036,7 +1058,7 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "599fe9aefc2ca0df4a96179b3075faee2cacb89d4cf947a00b9a89152dfffc9d"
 dependencies = [
- "base64",
+ "base64 0.13.1",
  "serde",
 ]
 
@@ -1054,11 +1076,11 @@ dependencies = [
 
 [[package]]
 name = "pem"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
+checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8"
 dependencies = [
- "base64",
+ "base64 0.13.1",
 ]
 
 [[package]]
@@ -1111,21 +1133,21 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
 name = "pkg-config"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.16"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
 [[package]]
 name = "prettyplease"
-version = "0.1.20"
+version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fead41e178796ef8274dc612a7d8ce4c7e10ca35cd2c5b5ad24cac63aeb6c0"
+checksum = "e97e3215779627f01ee256d2fad52f3d95e8e1c11e9fc6fd08f7cd455d5d5c78"
 dependencies = [
  "proc-macro2",
  "syn",
@@ -1133,18 +1155,18 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.46"
+version = "1.0.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
+checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "prost"
-version = "0.11.0"
+version = "0.11.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "399c3c31cdec40583bb68f0b18403400d01ec4289c383aa047560439952c4dd7"
+checksum = "21dc42e00223fc37204bd4aa177e69420c604ca4a183209a8f9de30c6d934698"
 dependencies = [
  "bytes",
  "prost-derive",
@@ -1152,9 +1174,9 @@ dependencies = [
 
 [[package]]
 name = "prost-build"
-version = "0.11.1"
+version = "0.11.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f835c582e6bd972ba8347313300219fed5bfa52caf175298d860b61ff6069bb"
+checksum = "a3f8ad728fb08fe212df3c05169e940fbb6d9d16a877ddde14644a983ba2012e"
 dependencies = [
  "bytes",
  "heck",
@@ -1163,18 +1185,20 @@ dependencies = [
  "log",
  "multimap",
  "petgraph",
+ "prettyplease",
  "prost",
  "prost-types",
  "regex",
+ "syn",
  "tempfile",
  "which",
 ]
 
 [[package]]
 name = "prost-derive"
-version = "0.11.0"
+version = "0.11.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7345d5f0e08c0536d7ac7229952590239e77abf0a0100a1b1d890add6ea96364"
+checksum = "8bda8c0881ea9f722eb9629376db3d0b903b462477c1aafcb0566610ac28ac5d"
 dependencies = [
  "anyhow",
  "itertools",
@@ -1185,9 +1209,9 @@ dependencies = [
 
 [[package]]
 name = "prost-types"
-version = "0.11.1"
+version = "0.11.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dfaa718ad76a44b3415e6c4d53b17c8f99160dcb3a99b10470fce8ad43f6e3e"
+checksum = "a5e0526209433e96d83d750dd81a99118edbc55739e7e61a46764fd2ad537788"
 dependencies = [
  "bytes",
  "prost",
@@ -1195,14 +1219,16 @@ dependencies = [
 
 [[package]]
 name = "pyo3"
-version = "0.17.2"
+version = "0.17.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "201b6887e5576bf2f945fe65172c1fcbf3fcf285b23e4d71eb171d9736e38d32"
+checksum = "268be0c73583c183f2b14052337465768c07726936a260f480f0857cb95ba543"
 dependencies = [
  "cfg-if",
  "indoc",
+ "inventory",
  "libc",
  "memoffset",
+ "num-complex",
  "parking_lot",
  "pyo3-build-config",
  "pyo3-ffi",
@@ -1225,9 +1251,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-build-config"
-version = "0.17.2"
+version = "0.17.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf0708c9ed01692635cbf056e286008e5a2927ab1a5e48cdd3aeb1ba5a6fef47"
+checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8"
 dependencies = [
  "once_cell",
  "target-lexicon",
@@ -1235,9 +1261,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-ffi"
-version = "0.17.2"
+version = "0.17.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90352dea4f486932b72ddf776264d293f85b79a1d214de1d023927b41461132d"
+checksum = "0f6cb136e222e49115b3c51c32792886defbfb0adead26a688142b346a0b9ffc"
 dependencies = [
  "libc",
  "pyo3-build-config",
@@ -1245,9 +1271,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-macros"
-version = "0.17.2"
+version = "0.17.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7eb24b804a2d9e88bfcc480a5a6dd76f006c1e3edaf064e8250423336e2cd79d"
+checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28"
 dependencies = [
  "proc-macro2",
  "pyo3-macros-backend",
@@ -1257,28 +1283,18 @@ dependencies = [
 
 [[package]]
 name = "pyo3-macros-backend"
-version = "0.17.2"
+version = "0.17.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f22bb49f6a7348c253d7ac67a6875f2dc65f36c2ae64a82c381d528972bea6d6"
+checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn",
 ]
 
-[[package]]
-name = "pythonize"
-version = "0.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f7f0c136f5fbc01868185eef462800e49659eb23acca83b9e884367a006acb6"
-dependencies = [
- "pyo3",
- "serde",
-]
-
 [[package]]
 name = "qcs"
-version = "0.8.3"
+version = "0.10.0-rc.1"
 dependencies = [
  "dirs",
  "enum-as-inner",
@@ -1296,6 +1312,7 @@ dependencies = [
  "qcs-api-client-grpc",
  "qcs-api-client-openapi",
  "quil-rs",
+ "regex",
  "reqwest",
  "rmp-serde",
  "serde",
@@ -1305,7 +1322,7 @@ dependencies = [
  "tempfile",
  "thiserror",
  "tokio",
- "toml 0.5.9",
+ "toml 0.5.11",
  "tonic",
  "uuid",
  "warp",
@@ -1327,9 +1344,9 @@ dependencies = [
 
 [[package]]
 name = "qcs-api-client-common"
-version = "0.2.7"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a067d003dbf106c4a3ec821ac99bf41e508bddbba760e66e4124ea41619209b5"
+checksum = "13b09ac85ca2a9839e81d72c2cd8c7a0995984e52d5f78c0d0d6ad00da075589"
 dependencies = [
  "dirs",
  "futures",
@@ -1338,14 +1355,14 @@ dependencies = [
  "serde",
  "thiserror",
  "tokio",
- "toml 0.5.9",
+ "toml 0.5.11",
 ]
 
 [[package]]
 name = "qcs-api-client-grpc"
-version = "0.2.7"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a77d9d4329f3d8a051417f96a69326965dadb21b10de83cd8c94018d4917b2c8"
+checksum = "623a691262ebdb46b72d1797682e2cc5fba1c859cacc17d58a2b59c9efe9ac77"
 dependencies = [
  "http-body",
  "pbjson",
@@ -1362,9 +1379,9 @@ dependencies = [
 
 [[package]]
 name = "qcs-api-client-openapi"
-version = "0.3.7"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b28810a4421531e887f846e071bb78ea8c5b57c13894c0a40c4919f5f530f908"
+checksum = "8a99306535343c8d28a013fe7489a9c52c7aeb4d64029f98444f44dadacc8334"
 dependencies = [
  "qcs-api-client-common",
  "reqwest",
@@ -1375,31 +1392,26 @@ dependencies = [
 
 [[package]]
 name = "qcs-sdk-python"
-version = "0.3.3"
+version = "0.5.0-rc.1"
 dependencies = [
  "pyo3",
  "pyo3-asyncio",
  "pyo3-build-config",
- "pythonize",
  "qcs",
  "qcs-api",
  "qcs-api-client-common",
+ "qcs-api-client-grpc",
  "quil-rs",
+ "rigetti-pyo3",
  "serde_json",
  "tokio",
 ]
 
-[[package]]
-name = "quick-error"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-
 [[package]]
 name = "quil-rs"
-version = "0.14.0"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3e0177b20a1153ef459e63e21815a64a4a442c107edb55c831142c68e481988"
+checksum = "430ad6ba33ade7e15fbd49c862b9433ecd1d0c8766046bdb42326038fbe5dbfa"
 dependencies = [
  "indexmap",
  "lexical",
@@ -1414,9 +1426,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.21"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
 dependencies = [
  "proc-macro2",
 ]
@@ -1473,18 +1485,20 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.6.0"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
 dependencies = [
+ "aho-corasick",
+ "memchr",
  "regex-syntax",
 ]
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.27"
+version = "0.6.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
 
 [[package]]
 name = "remove_dir_all"
@@ -1497,11 +1511,11 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.12"
+version = "0.11.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
+checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
 dependencies = [
- "base64",
+ "base64 0.21.0",
  "bytes",
  "encoding_rs",
  "futures-core",
@@ -1520,7 +1534,7 @@ dependencies = [
  "percent-encoding",
  "pin-project-lite",
  "rustls",
- "rustls-pemfile 1.0.1",
+ "rustls-pemfile 1.0.2",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -1535,6 +1549,19 @@ dependencies = [
  "winreg",
 ]
 
+[[package]]
+name = "rigetti-pyo3"
+version = "0.1.0-rc.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17d3d8822e4d3ab36de8cff3b8d9990a9d2ccc9c9da3536de9347e29353b2455"
+dependencies = [
+ "num-complex",
+ "num-traits",
+ "paste",
+ "pyo3",
+ "time",
+]
+
 [[package]]
 name = "ring"
 version = "0.16.20"
@@ -1574,9 +1601,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.20.6"
+version = "0.20.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
+checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
 dependencies = [
  "log",
  "ring",
@@ -1591,7 +1618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
 dependencies = [
  "openssl-probe",
- "rustls-pemfile 1.0.1",
+ "rustls-pemfile 1.0.2",
  "schannel",
  "security-framework",
 ]
@@ -1602,51 +1629,44 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
 dependencies = [
- "base64",
+ "base64 0.13.1",
 ]
 
 [[package]]
 name = "rustls-pemfile"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
+checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
 dependencies = [
- "base64",
+ "base64 0.21.0",
 ]
 
 [[package]]
 name = "rustversion"
-version = "1.0.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
-
-[[package]]
-name = "ryu"
 version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
 
 [[package]]
-name = "safemem"
-version = "0.3.3"
+name = "ryu"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
 
 [[package]]
 name = "schannel"
-version = "0.1.20"
+version = "0.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
 dependencies = [
- "lazy_static",
  "windows-sys",
 ]
 
 [[package]]
 name = "scoped-tls"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
 
 [[package]]
 name = "scopeguard"
@@ -1666,9 +1686,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework"
-version = "2.7.0"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
+checksum = "645926f31b250a2dca3c232496c2d898d91036e45ca0e97e0e2390c54e11be36"
 dependencies = [
  "bitflags",
  "core-foundation",
@@ -1679,9 +1699,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -1689,27 +1709,27 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.145"
+version = "1.0.152"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_bytes"
-version = "0.11.7"
+version = "0.11.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b"
+checksum = "718dc5fff5b36f99093fc49b280cfc96ce6fc824317783bff5a1fed0c7a64819"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.145"
+version = "1.0.152"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1718,9 +1738,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.86"
+version = "1.0.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
+checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
 dependencies = [
  "itoa",
  "ryu",
@@ -1739,17 +1759,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "sha-1"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
 [[package]]
 name = "sha1"
 version = "0.10.5"
@@ -1854,9 +1863,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.102"
+version = "1.0.107"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1871,9 +1880,9 @@ checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
 
 [[package]]
 name = "target-lexicon"
-version = "0.12.4"
+version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1"
+checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d"
 
 [[package]]
 name = "tempfile"
@@ -1891,18 +1900,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.37"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.37"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1911,21 +1920,30 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.15"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c"
+checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
 dependencies = [
  "itoa",
- "libc",
- "num_threads",
+ "serde",
+ "time-core",
  "time-macros",
 ]
 
+[[package]]
+name = "time-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
 [[package]]
 name = "time-macros"
-version = "0.2.4"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
+checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
+dependencies = [
+ "time-core",
+]
 
 [[package]]
 name = "tinyvec"
@@ -1944,9 +1962,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.21.2"
+version = "1.24.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
+checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb"
 dependencies = [
  "autocfg",
  "bytes",
@@ -1959,7 +1977,7 @@ dependencies = [
  "signal-hook-registry",
  "socket2",
  "tokio-macros",
- "winapi",
+ "windows-sys",
 ]
 
 [[package]]
@@ -1974,9 +1992,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "1.8.0"
+version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2005,18 +2023,6 @@ dependencies = [
  "tokio",
 ]
 
-[[package]]
-name = "tokio-tungstenite"
-version = "0.17.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
-dependencies = [
- "futures-util",
- "log",
- "tokio",
- "tungstenite",
-]
-
 [[package]]
 name = "tokio-util"
 version = "0.7.4"
@@ -2039,23 +2045,23 @@ checksum = "736b60249cb25337bc196faa43ee12c705e426f3d55c214d73a4e7be06f92cb4"
 
 [[package]]
 name = "toml"
-version = "0.5.9"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "tonic"
-version = "0.8.2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55b9af819e54b8f33d453655bef9b9acc171568fb49523078d0cc4e7484200ec"
+checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb"
 dependencies = [
  "async-stream",
  "async-trait",
  "axum",
- "base64",
+ "base64 0.13.1",
  "bytes",
  "futures-core",
  "futures-util",
@@ -2069,7 +2075,7 @@ dependencies = [
  "prost",
  "prost-derive",
  "rustls-native-certs",
- "rustls-pemfile 1.0.1",
+ "rustls-pemfile 1.0.2",
  "tokio",
  "tokio-rustls",
  "tokio-stream",
@@ -2083,9 +2089,9 @@ dependencies = [
 
 [[package]]
 name = "tonic-build"
-version = "0.8.2"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c6fd7c2581e36d63388a9e04c350c21beb7a8b059580b2e93993c526899ddc"
+checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4"
 dependencies = [
  "prettyplease",
  "proc-macro2",
@@ -2116,9 +2122,9 @@ dependencies = [
 
 [[package]]
 name = "tower-http"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba"
+checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858"
 dependencies = [
  "bitflags",
  "bytes",
@@ -2190,43 +2196,15 @@ dependencies = [
 
 [[package]]
 name = "try-lock"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
-
-[[package]]
-name = "tungstenite"
-version = "0.17.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
-dependencies = [
- "base64",
- "byteorder",
- "bytes",
- "http",
- "httparse",
- "log",
- "rand",
- "sha-1",
- "thiserror",
- "url",
- "utf-8",
-]
-
-[[package]]
-name = "twoway"
-version = "0.1.8"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
-dependencies = [
- "memchr",
-]
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 
 [[package]]
 name = "typenum"
-version = "1.15.0"
+version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
 
 [[package]]
 name = "unicase"
@@ -2239,15 +2217,15 @@ dependencies = [
 
 [[package]]
 name = "unicode-bidi"
-version = "0.3.8"
+version = "0.3.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
 
 [[package]]
 name = "unicode-normalization"
@@ -2260,9 +2238,9 @@ dependencies = [
 
 [[package]]
 name = "unindent"
-version = "0.1.10"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58ee9362deb4a96cef4d437d1ad49cffc9b9e92d202b6995674e928ce684f112"
+checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c"
 
 [[package]]
 name = "untrusted"
@@ -2281,17 +2259,11 @@ dependencies = [
  "percent-encoding",
 ]
 
-[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
 [[package]]
 name = "uuid"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
+checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
 dependencies = [
  "getrandom",
 ]
@@ -2327,7 +2299,6 @@ dependencies = [
  "log",
  "mime",
  "mime_guess",
- "multipart",
  "percent-encoding",
  "pin-project",
  "rustls-pemfile 0.2.1",
@@ -2337,7 +2308,6 @@ dependencies = [
  "serde_urlencoded",
  "tokio",
  "tokio-stream",
- "tokio-tungstenite",
  "tokio-util",
  "tower-service",
  "tracing",
@@ -2437,18 +2407,18 @@ dependencies = [
 
 [[package]]
 name = "webpki-roots"
-version = "0.22.5"
+version = "0.22.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be"
+checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
 dependencies = [
  "webpki",
 ]
 
 [[package]]
 name = "which"
-version = "4.3.0"
+version = "4.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
 dependencies = [
  "either",
  "libc",
@@ -2479,46 +2449,60 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "windows-sys"
-version = "0.36.1"
+version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
 dependencies = [
+ "windows_aarch64_gnullvm",
  "windows_aarch64_msvc",
  "windows_i686_gnu",
  "windows_i686_msvc",
  "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
  "windows_x86_64_msvc",
 ]
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.36.1"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.36.1"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.36.1"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.36.1"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.36.1"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
 
 [[package]]
 name = "winreg"
diff --git a/README.md b/README.md
index 2c3230e1e..c1a39e260 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ To build the docs.rs-style docs, run `makers docs`. You can also do `makers serv
 
 ## Release
 
-To release the library crate or the bindings to Python, manually run the `release` or `release python` workflow in GitHub Actions, respectively.
+To release, manually run the `Prepare Release` workflow in GitHub Actions.
 
 Pre-releases for both the library crate and Python package happen automatically on merge to main.
 
diff --git a/crates/lib/CHANGELOG.md b/crates/lib/CHANGELOG.md
index c0841ccff..00627d9dc 100644
--- a/crates/lib/CHANGELOG.md
+++ b/crates/lib/CHANGELOG.md
@@ -1,3 +1,63 @@
+## 0.10.0-rc.1
+
+### Breaking Changes
+
+- implement Python wrappers for the full Rust API (#230)
+
+## 0.10.0-rc.0
+
+### Breaking Changes
+
+- implement Python wrappers for the full Rust API (#230)
+
+## 0.9.2
+
+### Fixes
+
+- bump quil-rs to fix waveform issues (#228)
+
+## 0.9.2-rc.0
+
+### Fixes
+
+- bump quil-rs to fix waveform issues (#228)
+
+## 0.9.1
+
+### Fixes
+
+- Fetch Gateway address with pagination (#225)
+
+## 0.9.1-rc.0
+
+### Fixes
+
+- Fetch Gateway address with pagination (#225)
+
+## 0.9.0
+
+### Breaking Changes
+
+- compiler timeout is now configurable
+
+## 0.9.0-rc.0
+
+### Breaking Changes
+
+- compiler timeout is now configurable
+
+## 0.8.4
+
+### Features
+
+- add method for getting the version of the running quilc server (#206)
+
+## 0.8.4-rc.0
+
+### Features
+
+- add method for getting the version of the running quilc server (#206)
+
 ## 0.8.3
 
 ### Fixes
diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml
index a22cdfc54..347f3f3ff 100644
--- a/crates/lib/Cargo.toml
+++ b/crates/lib/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "qcs"
 description = "High level interface for running Quil on a QPU"
-version = "0.8.3"
+version = "0.10.0-rc.1"
 edition = "2018"
 license = "Apache-2.0"
 repository = "https://github.com/rigetti/qcs-sdk-rust"
@@ -19,19 +19,19 @@ futures = "0.3.24"
 indexmap = "1.9.1"
 lazy_static = "1.4.0"
 log = "0.4.17"
-num = "0.4.0"
+num = { version = "0.4.0", features = ["serde"] }
 qcs-api = "0.2.1"
-qcs-api-client-common = "0.2.7"
-qcs-api-client-openapi = "0.3.7"
-qcs-api-client-grpc = "0.2.7"
-quil-rs = "0.14"
+qcs-api-client-common = "0.3.0"
+qcs-api-client-openapi = "0.4.0"
+qcs-api-client-grpc = "0.3.0"
+quil-rs = "0.15"
 reqwest = { version = "0.11.12", default-features = false, features = ["rustls-tls", "json"] }
 rmp-serde = "1.1.1"
 serde = { version = "1.0.145", features = ["derive"] }
 serde_bytes = "0.11.7"
 serde_json = "1.0.86"
 thiserror = "1.0.37"
-tokio = { version = "1.21.2", features = ["fs"] }
+tokio = { version = "1.24.2", features = ["fs"] }
 toml = "0.5.9"
 uuid = { version = "1.2.1", features = ["v4"] }
 tonic = { version = "0.8.2", features = ["tls", "tls-roots"] }
@@ -42,8 +42,9 @@ erased-serde = "0.3.23"
 float-cmp = "0.9.0"
 hex = "0.4.3"
 maplit = "1.0.2"
-qcs-api-client-grpc = { version = "0.2.7", features = ["server"] }
+qcs-api-client-grpc = { version = "0.3.0", features = ["server"] }
 simple_logger = { version = "2.3.0", default-features = false }
 tempfile = "3.3.0"
 tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] }
-warp = "0.3.3"
+warp = { version = "0.3.3", default-features = false }
+regex = "1.7.0"
diff --git a/crates/lib/src/api.rs b/crates/lib/src/api.rs
index 5d88bef0e..7da21c3b6 100644
--- a/crates/lib/src/api.rs
+++ b/crates/lib/src/api.rs
@@ -1,40 +1,54 @@
 //! This module provides convenience functions to handle compilation,
 //! translation, parameter arithmetic rewriting, and results collection.
 
-use std::{collections::HashMap, str::FromStr};
+use std::{collections::HashMap, str::FromStr, time::Duration};
 
+use num::Complex;
 use qcs_api_client_grpc::{
     models::controller::{readout_values, ControllerJobExecutionResult},
     services::controller::{
         get_controller_job_results_request::Target, GetControllerJobResultsRequest,
     },
 };
+use qcs_api_client_openapi::apis::{quantum_processors_api, Error as OpenAPIError};
 use quil_rs::expression::Expression;
 use quil_rs::{program::ProgramError, Program};
 use serde::Serialize;
+use tokio::time::error::Elapsed;
 
 use crate::qpu::{
     self,
     client::{GrpcClientError, Qcs},
-    quilc::{self, TargetDevice},
+    quilc::{self, CompilerOpts, TargetDevice},
     rewrite_arithmetic::{self, Substitutions},
     runner,
     translation::{self, EncryptedTranslationResult},
     IsaError,
 };
 
+/// TODO: make configurable at the client level.
+/// <https://github.com/rigetti/qcs-sdk-rust/issues/239>
+static DEFAULT_HTTP_API_TIMEOUT: Duration = Duration::from_secs(10);
+
 /// Uses quilc to convert a Quil program to native Quil
 pub fn compile(
     quil: &str,
     target: TargetDevice,
-    protoquil: Option<bool>,
     client: &Qcs,
-) -> Result<String, Box<dyn std::error::Error>> {
-    quilc::compile_program(quil, target, protoquil, client)
+    options: CompilerOpts,
+) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
+    quilc::compile_program(quil, target, client, options)
         .map_err(Into::into)
         .map(|p| p.to_string(true))
 }
 
+/// Gets the quilc version
+pub fn get_quilc_version(
+    client: &Qcs,
+) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
+    quilc::get_version_info(client).map_err(Into::into)
+}
+
 /// Collection of errors that can result from rewriting arithmetic.
 #[derive(thiserror::Error, Debug)]
 pub enum RewriteArithmeticError {
@@ -48,7 +62,7 @@ pub enum RewriteArithmeticError {
 
 /// The result of a call to [`rewrite_arithmetic`] which provides the
 /// information necessary to later patch-in memory values to a compiled program.
-#[derive(Serialize)]
+#[derive(Clone, Debug, Serialize)]
 pub struct RewriteArithmeticResult {
     /// The rewritten program
     pub program: String,
@@ -68,7 +82,7 @@ pub struct RewriteArithmeticResult {
 /// May return an error if the program fails to parse, or the parameter arithmetic
 /// cannot be rewritten.
 pub fn rewrite_arithmetic(
-    native_quil: quil_rs::Program,
+    native_quil: Program,
 ) -> Result<RewriteArithmeticResult, rewrite_arithmetic::Error> {
     let (program, subs) = qpu::rewrite_arithmetic::rewrite_arithmetic(native_quil)?;
     let recalculation_table = subs.into_iter().map(|expr| expr.to_string()).collect();
@@ -186,17 +200,13 @@ pub fn build_patch_values(
         .iter()
         .map(|expr| Expression::from_str(expr))
         .collect::<Result<_, _>>()
-        .map_err(|e| format!("Unable to interpret recalculation table: {:?}", e))?;
+        .map_err(|e| format!("Unable to interpret recalculation table: {e:?}"))?;
     rewrite_arithmetic::get_substitutions(&substitutions, memory)
 }
 
-/// A convenience type that describes a Complex-64 value whose real
-/// and imaginary parts of both f32.
-pub type Complex64 = [f32; 2];
-
 /// Data from an individual register. Each variant contains a vector with the expected data type
 /// where each value in the vector corresponds to a shot.
-#[derive(Debug, PartialEq, Serialize)]
+#[derive(Clone, Debug, PartialEq, Serialize)]
 #[serde(untagged)] // Don't include the discriminant name in serialized output.
 pub enum Register {
     /// A register of 64-bit floating point numbers
@@ -206,7 +216,7 @@ pub enum Register {
     /// A register of 32-bit integers
     I32(Vec<i32>),
     /// A register of 64-bit complex numbers
-    Complex64(Vec<Complex64>),
+    Complex64(Vec<Complex<f32>>),
     /// A register of 8-bit integers (bytes)
     I8(Vec<i8>),
 }
@@ -217,7 +227,7 @@ impl From<qpu::runner::Register> for Register {
             runner::Register::F64(f) => Register::F64(f),
             runner::Register::I16(i) => Register::I16(i),
             runner::Register::Complex32(c) => {
-                Register::Complex64(c.iter().map(|c| [c.re, c.im]).collect())
+                Register::Complex64(c.iter().map(|c| Complex::<f32>::new(c.re, c.im)).collect())
             }
             runner::Register::I8(i) => Register::I8(i),
         }
@@ -225,7 +235,7 @@ impl From<qpu::runner::Register> for Register {
 }
 
 /// The execution readout data from a particular memory location.
-#[derive(Serialize)]
+#[derive(Clone, Debug, Serialize)]
 pub struct ExecutionResult {
     shape: Vec<usize>,
     data: Register,
@@ -241,7 +251,9 @@ impl From<readout_values::Values> for ExecutionResult {
                 data: Register::Complex64(
                     c.values
                         .iter()
-                        .map(|c| [c.real.unwrap_or(0.0), c.imaginary.unwrap_or(0.0)])
+                        .map(|c| {
+                            Complex::<f32>::new(c.real.unwrap_or(0.0), c.imaginary.unwrap_or(0.0))
+                        })
                         .collect(),
                 ),
             },
@@ -255,7 +267,7 @@ impl From<readout_values::Values> for ExecutionResult {
 }
 
 /// Execution readout data for all memory locations.
-#[derive(Serialize)]
+#[derive(Clone, Debug, Serialize)]
 pub struct ExecutionResults {
     buffers: HashMap<String, ExecutionResult>,
     execution_duration_microseconds: Option<u64>,
@@ -307,3 +319,53 @@ pub async fn retrieve_results(
         .map(ExecutionResults::from)
         .ok_or_else(|| GrpcClientError::ResponseEmpty("Controller Job Execution Results".into()))
 }
+
+/// API Errors encountered when trying to list available quantum processors.
+#[derive(Debug, thiserror::Error)]
+pub enum ListQuantumProcessorsError {
+    /// Failed the http call
+    #[error("Failed to list processors via API: {0}")]
+    ApiError(#[from] OpenAPIError<quantum_processors_api::ListQuantumProcessorsError>),
+
+    /// Pagination did not finish before timeout
+    #[error("API pagination did not finish before timeout: {0:?}")]
+    TimeoutError(#[from] Elapsed),
+}
+
+/// Query the QCS API for the names of all available quantum processors.
+/// If `None`, the default `timeout` used is 10 seconds.
+pub async fn list_quantum_processors(
+    client: &Qcs,
+    timeout: Option<Duration>,
+) -> Result<Vec<String>, ListQuantumProcessorsError> {
+    let timeout = timeout.unwrap_or(DEFAULT_HTTP_API_TIMEOUT);
+
+    tokio::time::timeout(timeout, async move {
+        let mut quantum_processors = vec![];
+        let mut page_token = None;
+
+        loop {
+            let result = quantum_processors_api::list_quantum_processors(
+                &client.get_openapi_client(),
+                Some(100),
+                page_token.as_deref(),
+            )
+            .await?;
+
+            let mut data = result
+                .quantum_processors
+                .into_iter()
+                .map(|qpu| qpu.id)
+                .collect::<Vec<_>>();
+            quantum_processors.append(&mut data);
+
+            page_token = result.next_page_token;
+            if page_token.is_none() {
+                break;
+            }
+        }
+
+        Ok(quantum_processors)
+    })
+    .await?
+}
diff --git a/crates/lib/src/configuration/mod.rs b/crates/lib/src/configuration/mod.rs
deleted file mode 100644
index 50513e0e0..000000000
--- a/crates/lib/src/configuration/mod.rs
+++ /dev/null
@@ -1,198 +0,0 @@
-//! This module is used for loading configuration that will be used to connect either to real QPUs
-//! (and supporting services) or the QVM.
-//!
-//! By default, all settings are loaded from files located
-//! under your home directory in a `.qcs` folder. `settings.toml` will be used to load general
-//! settings (e.g. which URLs to connect to) and `secrets.toml` will be used to load tokens for
-//! authentication. Both "settings" and "secrets" files should contain profiles. The
-//! `default_profile_name` in settings sets the profile to be used when there is no override. You
-//! can set the [`PROFILE_NAME_VAR`] to select a different profile. You can also use
-//! [`SECRETS_PATH_VAR`] and [`SETTINGS_PATH_VAR`] to change which files are loaded.
-
-use std::path::PathBuf;
-
-use futures::future::try_join;
-use serde::{Deserialize, Serialize};
-
-use qcs_api::apis::configuration as api;
-use secrets::Secrets;
-pub use secrets::SECRETS_PATH_VAR;
-pub use settings::SETTINGS_PATH_VAR;
-use settings::{AuthServer, Pyquil, Settings};
-
-use crate::configuration::LoadError::AuthServerNotFound;
-
-mod path;
-mod secrets;
-mod settings;
-
-/// All the config data that's parsed from config sources
-#[derive(Clone, Debug)]
-pub struct Configuration {
-    api_config: api::Configuration,
-    auth_server: AuthServer,
-    refresh_token: Option<String>,
-    /// The URL for the quilc server.
-    pub quilc_url: String,
-    /// The URL for the QVM server.
-    pub qvm_url: String,
-}
-
-/// Setting this environment variable will change which profile is used from the loaded config files
-pub const PROFILE_NAME_VAR: &str = "QCS_PROFILE_NAME";
-
-/// Errors raised when attempting to refresh the user's token
-#[derive(Debug, thiserror::Error)]
-pub enum RefreshError {
-    /// Error due to no token available to refresh.
-    #[error("No refresh token is in secrets")]
-    NoRefreshToken,
-    /// Error when trying to fetch the new token.
-    #[error("Error fetching new token")]
-    FetchError(#[from] reqwest::Error),
-}
-
-/// Errors raised when attempting to load the user's configuration files.
-#[derive(Debug, thiserror::Error)]
-pub enum LoadError {
-    /// Error due to requested profile missing in the user's configuration.
-    #[error("Expected profile {0} in settings.profiles but it didn't exist")]
-    ProfileNotFound(String),
-    /// Error due to authentication server missing in the user's configuration.
-    #[error("Expected auth server {0} in settings.auth_servers but it didn't exist")]
-    AuthServerNotFound(String),
-    /// Error due to failing to find the user's home directory.
-    #[error("Failed to determine home directory. You can use an explicit path by setting the {env} environment variable")]
-    HomeDirError {
-        /// Environment variable to set to configure the home directory.
-        env: String,
-    },
-    /// Error due to failing to open a file.
-    #[error("Could not open file at {path}")]
-    FileOpenError {
-        /// Path to file that could not be opened.
-        path: PathBuf,
-        /// The source error.
-        source: std::io::Error,
-    },
-    /// Error due to failing to parse a file.
-    #[error("Could not parse file at {path}")]
-    FileParseError {
-        /// Path to file that could not be parsed.
-        path: PathBuf,
-        /// Source error.
-        source: toml::de::Error,
-    },
-}
-
-impl Configuration {
-    /// Attempt to load config files from ~/.qcs and create a Configuration object
-    /// for use with qcs-api.
-    ///
-    /// # Errors
-    ///
-    /// See [`LoadError`].
-    pub async fn load() -> Result<Self, LoadError> {
-        let (settings, secrets) = try_join(settings::load(), secrets::load()).await?;
-        Self::new(settings, secrets)
-    }
-
-    /// Refresh the `access_token` and return a new `Configuration` if successful.
-    ///
-    /// # Errors
-    ///
-    /// See [`RefreshError`].
-    pub async fn refresh(mut self) -> Result<Self, RefreshError> {
-        let refresh_token = self.refresh_token.ok_or(RefreshError::NoRefreshToken)?;
-        let token_url = format!("{}/v1/token", &self.auth_server.issuer);
-        let data = TokenRequest::new(&self.auth_server.client_id, &refresh_token);
-        let resp = self
-            .api_config
-            .client
-            .post(token_url)
-            .form(&data)
-            .send()
-            .await?;
-        let response_data: TokenResponse = resp.error_for_status()?.json().await?;
-        self.api_config.bearer_access_token = Some(response_data.access_token);
-        self.refresh_token = Some(response_data.refresh_token);
-        Ok(self)
-    }
-
-    fn new(settings: Settings, mut secrets: Secrets) -> Result<Self, LoadError> {
-        let Settings {
-            default_profile_name,
-            mut profiles,
-            mut auth_servers,
-        } = settings;
-        let profile_name = std::env::var(PROFILE_NAME_VAR).unwrap_or(default_profile_name);
-        let profile = profiles
-            .remove(&profile_name)
-            .ok_or(LoadError::ProfileNotFound(profile_name))?;
-        let auth_server = auth_servers
-            .remove(&profile.auth_server_name)
-            .ok_or_else(|| AuthServerNotFound(profile.auth_server_name.clone()))?;
-
-        let credential = secrets.credentials.remove(&profile.credentials_name);
-        let (access_token, refresh_token) = match credential {
-            Some(secrets::Credential {
-                token_payload: Some(token_payload),
-            }) => (token_payload.access_token, token_payload.refresh_token),
-            _ => (None, None),
-        };
-
-        Ok(Self {
-            api_config: api::Configuration {
-                base_path: profile.api_url,
-                bearer_access_token: access_token,
-                ..api::Configuration::default()
-            },
-            auth_server,
-            refresh_token,
-            quilc_url: profile.applications.pyquil.quilc_url,
-            qvm_url: profile.applications.pyquil.qvm_url,
-        })
-    }
-}
-
-#[derive(Debug, Serialize, Copy, Clone, Eq, PartialEq)]
-struct TokenRequest<'a> {
-    grant_type: &'static str,
-    client_id: &'a str,
-    refresh_token: &'a str,
-}
-
-impl<'a> TokenRequest<'a> {
-    fn new(client_id: &'a str, refresh_token: &'a str) -> TokenRequest<'a> {
-        Self {
-            grant_type: "refresh_token",
-            client_id,
-            refresh_token,
-        }
-    }
-}
-
-#[derive(Deserialize, Debug, Clone, Eq, PartialEq)]
-struct TokenResponse {
-    refresh_token: String,
-    access_token: String,
-}
-
-impl AsRef<api::Configuration> for Configuration {
-    fn as_ref(&self) -> &api::Configuration {
-        &self.api_config
-    }
-}
-
-impl Default for Configuration {
-    fn default() -> Self {
-        let Pyquil { quilc_url, qvm_url } = Pyquil::default();
-        Self {
-            quilc_url,
-            qvm_url,
-            api_config: api::Configuration::default(),
-            auth_server: AuthServer::default(),
-            refresh_token: None,
-        }
-    }
-}
diff --git a/crates/lib/src/configuration/path.rs b/crates/lib/src/configuration/path.rs
deleted file mode 100644
index fbd5b2c6b..000000000
--- a/crates/lib/src/configuration/path.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use std::path::PathBuf;
-
-use super::LoadError;
-
-pub(crate) fn path_from_env_or_home(env: &str, file_name: &str) -> Result<PathBuf, LoadError> {
-    match std::env::var(env) {
-        Ok(path) => Ok(PathBuf::from(path)),
-        Err(_) => dirs::home_dir()
-            .map(|path| path.join(".qcs").join(file_name))
-            .ok_or_else(|| LoadError::HomeDirError {
-                env: env.to_string(),
-            }),
-    }
-}
diff --git a/crates/lib/src/configuration/secrets.rs b/crates/lib/src/configuration/secrets.rs
deleted file mode 100644
index 31690a2df..000000000
--- a/crates/lib/src/configuration/secrets.rs
+++ /dev/null
@@ -1,93 +0,0 @@
-use std::collections::HashMap;
-
-use serde::{Deserialize, Serialize};
-
-use crate::configuration::LoadError;
-
-use super::path::path_from_env_or_home;
-
-/// Setting the `QCS_SECRETS_FILE_PATH` environment variable will change which file is used for loading secrets
-pub const SECRETS_PATH_VAR: &str = "QCS_SECRETS_FILE_PATH";
-
-pub(crate) async fn load() -> Result<Secrets, LoadError> {
-    let path = path_from_env_or_home(SECRETS_PATH_VAR, "secrets.toml")?;
-    let content =
-        tokio::fs::read_to_string(&path)
-            .await
-            .map_err(|source| LoadError::FileOpenError {
-                path: path.clone(),
-                source,
-            })?;
-    toml::from_str(&content).map_err(|source| LoadError::FileParseError { path, source })
-}
-
-#[cfg(test)]
-mod describe_load {
-    use std::io::Write;
-
-    use tempfile::NamedTempFile;
-
-    use super::{load, Credential, Secrets, SECRETS_PATH_VAR};
-
-    #[tokio::test]
-    async fn it_returns_default_if_missing_path() {
-        std::env::set_var(SECRETS_PATH_VAR, "/blah/doesnt_exist.toml");
-
-        let settings = load().await;
-
-        std::env::remove_var(SECRETS_PATH_VAR);
-        assert!(settings.is_err());
-    }
-
-    #[tokio::test]
-    async fn it_loads_from_env_var_path() {
-        let mut file = NamedTempFile::new().expect("Failed to create temporary settings file");
-        let mut secrets = Secrets::default();
-        secrets
-            .credentials
-            .insert("test".to_string(), Credential::default());
-        let secrets_string = toml::to_string(&secrets).expect("Could not serialize test settings");
-        let _ = file
-            .write(secrets_string.as_bytes())
-            .expect("Failed to write test settings");
-        std::env::set_var(SECRETS_PATH_VAR, file.path());
-
-        let loaded = load().await.expect("Failed to load secrets");
-
-        assert_eq!(secrets, loaded);
-    }
-}
-
-#[derive(Deserialize, Debug, PartialEq, Serialize, Clone, Eq)]
-pub(crate) struct Secrets {
-    pub credentials: HashMap<String, Credential>,
-}
-
-impl Default for Secrets {
-    fn default() -> Self {
-        Self {
-            credentials: default_credentials(),
-        }
-    }
-}
-
-fn default_credentials() -> HashMap<String, Credential> {
-    let mut map = HashMap::with_capacity(1);
-    map.insert("default".to_string(), Credential::default());
-    map
-}
-
-#[derive(Deserialize, Debug, Default, PartialEq, Serialize, Clone, Eq)]
-pub(crate) struct Credential {
-    pub token_payload: Option<TokenPayload>,
-}
-
-#[derive(Deserialize, Debug, Default, PartialEq, Serialize, Clone, Eq)]
-pub(crate) struct TokenPayload {
-    pub refresh_token: Option<String>,
-    pub access_token: Option<String>,
-    scope: Option<String>,
-    expires_in: Option<u32>,
-    id_token: Option<String>,
-    token_type: Option<String>,
-}
diff --git a/crates/lib/src/configuration/settings.rs b/crates/lib/src/configuration/settings.rs
deleted file mode 100644
index c7f9b8934..000000000
--- a/crates/lib/src/configuration/settings.rs
+++ /dev/null
@@ -1,151 +0,0 @@
-use std::collections::HashMap;
-
-use serde::{Deserialize, Serialize};
-
-use super::path::path_from_env_or_home;
-use super::LoadError;
-
-/// Setting the `QCS_SETTINGS_FILE_PATH` environment variable will change which file is used for loading settings
-pub const SETTINGS_PATH_VAR: &str = "QCS_SETTINGS_FILE_PATH";
-
-pub(crate) async fn load() -> Result<Settings, LoadError> {
-    let path = path_from_env_or_home(SETTINGS_PATH_VAR, "settings.toml")?;
-    let content =
-        tokio::fs::read_to_string(&path)
-            .await
-            .map_err(|source| LoadError::FileOpenError {
-                path: path.clone(),
-                source,
-            })?;
-    toml::from_str(&content).map_err(|source| LoadError::FileParseError { path, source })
-}
-
-#[cfg(test)]
-mod describe_load {
-    use std::io::Write;
-
-    use tempfile::NamedTempFile;
-
-    use crate::configuration::{settings::load, SETTINGS_PATH_VAR};
-
-    use super::Settings;
-
-    #[tokio::test]
-    async fn it_returns_default_if_missing_path() {
-        std::env::set_var(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
-
-        let settings = load().await;
-
-        std::env::remove_var(SETTINGS_PATH_VAR);
-
-        assert!(settings.is_err());
-    }
-
-    #[tokio::test]
-    async fn it_loads_from_env_var_path() {
-        let mut file = NamedTempFile::new().expect("Failed to create temporary settings file");
-        let settings = Settings {
-            default_profile_name: "THIS IS A TEST".to_string(),
-            ..Default::default()
-        };
-        let settings_string =
-            toml::to_string(&settings).expect("Could not serialize test settings");
-        let _ = file
-            .write(settings_string.as_bytes())
-            .expect("Failed to write test settings");
-        std::env::set_var(SETTINGS_PATH_VAR, file.path());
-
-        let loaded = load().await.expect("Failed to load settings");
-
-        assert_eq!(settings, loaded);
-    }
-}
-
-#[derive(Deserialize, Debug, PartialEq, Serialize, Clone, Eq)]
-pub(crate) struct Settings {
-    /// Which profile to select settings from when none is specified.
-    pub default_profile_name: String,
-    /// All available configuration profiles, keyed by profile name.
-    #[serde(default = "default_profiles")]
-    pub profiles: HashMap<String, Profile>,
-    #[serde(default)]
-    pub(crate) auth_servers: HashMap<String, AuthServer>,
-}
-
-impl Default for Settings {
-    fn default() -> Self {
-        Self {
-            default_profile_name: "default".to_string(),
-            profiles: default_profiles(),
-            auth_servers: default_auth_servers(),
-        }
-    }
-}
-
-fn default_profiles() -> HashMap<String, Profile> {
-    let mut map = HashMap::with_capacity(1);
-    map.insert("default".to_string(), Profile::default());
-    map
-}
-
-fn default_auth_servers() -> HashMap<String, AuthServer> {
-    let mut map = HashMap::with_capacity(1);
-    map.insert("default".to_string(), AuthServer::default());
-    map
-}
-
-#[derive(Deserialize, Debug, PartialEq, Serialize, Clone, Eq)]
-pub(crate) struct Profile {
-    /// URL of the QCS API to use for all API calls
-    pub api_url: String,
-    pub auth_server_name: String,
-    pub credentials_name: String,
-    #[serde(default)]
-    pub applications: Applications,
-}
-
-impl Default for Profile {
-    fn default() -> Self {
-        Self {
-            api_url: "https://api.qcs.rigetti.com".to_string(),
-            auth_server_name: "default".to_string(),
-            credentials_name: "default".to_string(),
-            applications: Applications::default(),
-        }
-    }
-}
-
-#[derive(Deserialize, Debug, Default, PartialEq, Serialize, Clone, Eq)]
-pub(crate) struct Applications {
-    pub pyquil: Pyquil,
-}
-
-#[derive(Deserialize, Debug, PartialEq, Serialize, Clone, Eq)]
-pub(crate) struct Pyquil {
-    pub qvm_url: String,
-    pub quilc_url: String,
-}
-
-impl Default for Pyquil {
-    fn default() -> Self {
-        Self {
-            qvm_url: "http://127.0.0.1:5000".to_string(),
-            quilc_url: "tcp://127.0.0.1:5555".to_string(),
-        }
-    }
-}
-
-#[derive(Clone, Deserialize, Debug, PartialEq, Serialize, Eq)]
-pub(crate) struct AuthServer {
-    pub(crate) client_id: String,
-    pub(crate) issuer: String,
-}
-
-impl Default for AuthServer {
-    fn default() -> Self {
-        Self {
-            client_id: "0oa3ykoirzDKpkfzk357".to_string(),
-            issuer: "https://auth.qcs.rigetti.com/oauth2/aus8jcovzG0gW2TUG355".to_string(),
-        }
-    }
-}
diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs
index d791f1377..78ea5c35a 100644
--- a/crates/lib/src/executable.rs
+++ b/crates/lib/src/executable.rs
@@ -1,6 +1,7 @@
 //! This module contains the public-facing API for executing programs. [`Executable`] is the how
 //! users will interact with QCS, quilc, and QVM.
 
+use std::borrow::Cow;
 use std::collections::HashMap;
 use std::sync::Arc;
 use std::time::Duration;
@@ -10,6 +11,7 @@ use qcs_api_client_common::ClientConfiguration;
 
 use crate::execution_data;
 use crate::qpu::client::Qcs;
+use crate::qpu::quilc::CompilerOpts;
 use crate::qpu::rewrite_arithmetic;
 use crate::qpu::runner::JobId;
 use crate::qpu::ExecutionError;
@@ -61,13 +63,14 @@ use quil_rs::Program;
 /// You should be able to largely ignore these, just keep in mind that any borrowed data passed to
 /// the methods most likely needs to live as long as this struct. Check individual methods for
 /// specifics. If only using `'static` strings then everything should just work.
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub struct Executable<'executable, 'execution> {
     quil: Arc<str>,
     shots: u16,
-    readout_memory_region_names: Option<Vec<&'executable str>>,
+    readout_memory_region_names: Option<Vec<Cow<'executable, str>>>,
     params: Parameters,
     compile_with_quilc: bool,
+    compiler_options: CompilerOpts,
     config: Option<ClientConfiguration>,
     client: Option<Arc<Qcs>>,
     qpu: Option<qpu::Execution<'execution>>,
@@ -99,6 +102,7 @@ impl<'executable> Executable<'executable, '_> {
             readout_memory_region_names: None,
             params: Parameters::new(),
             compile_with_quilc: true,
+            compiler_options: CompilerOpts::default(),
             config: None,
             client: None,
             qpu: None,
@@ -157,9 +161,12 @@ impl<'executable> Executable<'executable, '_> {
     /// }
     /// ```
     #[must_use]
-    pub fn read_from(mut self, register: &'executable str) -> Self {
+    pub fn read_from<S>(mut self, register: S) -> Self
+    where
+        S: Into<Cow<'executable, str>>,
+    {
         let mut readouts = self.readout_memory_region_names.take().unwrap_or_default();
-        readouts.push(register);
+        readouts.push(register.into());
         self.readout_memory_region_names = Some(readouts);
         self
     }
@@ -188,7 +195,7 @@ impl<'executable> Executable<'executable, '_> {
     ///     let mut exe = Executable::from_quil(PROGRAM)
     ///         .with_config(ClientConfiguration::default()) // Unnecessary if you have ~/.qcs/settings.toml
     ///         .read_from("theta");
-    ///     
+    ///
     ///     for theta in 0..2 {
     ///         let theta = theta as f64;
     ///         let mut result = exe
@@ -233,8 +240,10 @@ impl<'executable> Executable<'executable, '_> {
     }
 }
 
-type ExecuteResultQVM = Result<execution_data::Qvm, Error>;
-type ExecuteResultQPU = Result<execution_data::Qpu, Error>;
+/// The [`Result`] from executing on the QVM.
+pub type ExecuteResultQVM = Result<execution_data::Qvm, Error>;
+/// The [`Result`] from executing on a QPU.
+pub type ExecuteResultQPU = Result<execution_data::Qpu, Error>;
 
 impl Executable<'_, '_> {
     /// Specify a number of times to run the program for each execution. Defaults to 1 run or "shot".
@@ -252,11 +261,18 @@ impl Executable<'_, '_> {
         self
     }
 
-    fn get_readouts(&self) -> &[&str] {
+    /// If set, the value will override the default compiler options
+    #[must_use]
+    pub fn compiler_options(mut self, options: CompilerOpts) -> Self {
+        self.compiler_options = options;
+        self
+    }
+
+    fn get_readouts(&self) -> &[Cow<'_, str>] {
         return self
             .readout_memory_region_names
             .as_ref()
-            .map_or(&["ro"], Vec::as_slice);
+            .map_or(&[Cow::Borrowed("ro")], Vec::as_slice);
     }
 
     /// Execute on a QVM which must be available at the configured URL (default <http://localhost:5000>).
@@ -297,7 +313,7 @@ impl Executable<'_, '_> {
         if let Some(config) = &self.config {
             Ok(config.clone())
         } else {
-            let config = ClientConfiguration::load().await?;
+            let config = ClientConfiguration::load_default().await?;
             self.config = Some(config.clone());
             Ok(config)
         }
@@ -318,12 +334,13 @@ impl Executable<'_, '_> {
 
 impl<'execution> Executable<'_, 'execution> {
     /// Remove and return `self.qpu` if it's set and still valid. Otherwise, create a new one.
-    async fn qpu_for_id(
-        &mut self,
-        id: &'execution str,
-    ) -> Result<qpu::Execution<'execution>, Error> {
+    async fn qpu_for_id<S>(&mut self, id: S) -> Result<qpu::Execution<'execution>, Error>
+    where
+        S: Into<Cow<'execution, str>>,
+    {
+        let id = id.into();
         if let Some(qpu) = self.qpu.take() {
-            if qpu.quantum_processor_id == id && qpu.shots == self.shots {
+            if qpu.quantum_processor_id == id.as_ref() && qpu.shots == self.shots {
                 return Ok(qpu);
             }
         }
@@ -333,6 +350,7 @@ impl<'execution> Executable<'_, 'execution> {
             id,
             self.get_client().await?,
             self.compile_with_quilc,
+            self.compiler_options,
         )
         .await
         .map_err(Error::from)
@@ -364,10 +382,10 @@ impl<'execution> Executable<'_, 'execution> {
     /// 1. Missing parameters that should be filled with [`Executable::with_parameter`]
     ///
     /// [quilc]: https://github.com/quil-lang/quilc
-    pub async fn execute_on_qpu(
-        &mut self,
-        quantum_processor_id: &'execution str,
-    ) -> ExecuteResultQPU {
+    pub async fn execute_on_qpu<S>(&mut self, quantum_processor_id: S) -> ExecuteResultQPU
+    where
+        S: Into<Cow<'execution, str>>,
+    {
         let job_handle = self.submit_to_qpu(quantum_processor_id).await?;
         self.retrieve_results(job_handle).await
     }
@@ -380,16 +398,20 @@ impl<'execution> Executable<'_, 'execution> {
     /// # Errors
     ///
     /// See [`Executable::execute_on_qpu`].
-    pub async fn submit_to_qpu(
+    pub async fn submit_to_qpu<S>(
         &mut self,
-        quantum_processor_id: &'execution str,
-    ) -> Result<JobHandle<'execution>, Error> {
+        quantum_processor_id: S,
+    ) -> Result<JobHandle<'execution>, Error>
+    where
+        S: Into<Cow<'execution, str>>,
+    {
+        let quantum_processor_id = quantum_processor_id.into();
         let JobHandle {
             job_id,
             readout_map,
             ..
         } = self
-            .qpu_for_id(quantum_processor_id)
+            .qpu_for_id(quantum_processor_id.clone())
             .await?
             .submit(&self.params)
             .await?;
@@ -514,11 +536,11 @@ pub enum Service {
 impl From<ExecutionError> for Error {
     fn from(err: ExecutionError) -> Self {
         match err {
-            ExecutionError::Unexpected(inner) => Self::Unexpected(format!("{:?}", inner)),
+            ExecutionError::Unexpected(inner) => Self::Unexpected(format!("{inner:?}")),
             ExecutionError::Quilc { .. } => Self::Connection(Service::Quilc),
-            ExecutionError::QcsClient(v) => Self::Unexpected(format!("{:?}", v)),
-            ExecutionError::IsaError(v) => Self::Unexpected(format!("{:?}", v)),
-            ExecutionError::ReadoutParse(v) => Self::Unexpected(format!("{:?}", v)),
+            ExecutionError::QcsClient(v) => Self::Unexpected(format!("{v:?}")),
+            ExecutionError::IsaError(v) => Self::Unexpected(format!("{v:?}")),
+            ExecutionError::ReadoutParse(v) => Self::Unexpected(format!("{v:?}")),
             ExecutionError::Quil(e) => Self::Quil(e),
             ExecutionError::Compilation { details } => Self::Compilation(details),
             ExecutionError::RewriteArithmetic(e) => Self::RewriteArithmetic(e),
@@ -535,7 +557,7 @@ impl From<qvm::Error> for Error {
             | qvm::Error::ShotsMustBePositive
             | qvm::Error::RegionSizeMismatch { .. }
             | qvm::Error::RegionNotFound { .. }
-            | qvm::Error::Qvm { .. } => Self::Compilation(format!("{}", err)),
+            | qvm::Error::Qvm { .. } => Self::Compilation(format!("{err}")),
         }
     }
 }
@@ -545,20 +567,23 @@ impl From<qvm::Error> for Error {
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct JobHandle<'executable> {
     job_id: JobId,
-    quantum_processor_id: &'executable str,
+    quantum_processor_id: Cow<'executable, str>,
     readout_map: HashMap<String, String>,
 }
 
 impl<'a> JobHandle<'a> {
     #[must_use]
-    pub(crate) fn new(
+    pub(crate) fn new<S>(
         job_id: JobId,
-        quantum_processor_id: &'a str,
+        quantum_processor_id: S,
         readout_map: HashMap<String, String>,
-    ) -> Self {
+    ) -> Self
+    where
+        S: Into<Cow<'a, str>>,
+    {
         Self {
             job_id,
-            quantum_processor_id,
+            quantum_processor_id: quantum_processor_id.into(),
             readout_map,
         }
     }
@@ -586,7 +611,7 @@ mod describe_get_config {
     #[tokio::test]
     async fn it_resizes_params_dynamically() {
         let mut exe = Executable::from_quil("").with_config(ClientConfiguration::default());
-        let foo_len = |exe: &mut Executable| exe.params.get("foo").unwrap().len();
+        let foo_len = |exe: &mut Executable<'_, '_>| exe.params.get("foo").unwrap().len();
 
         exe.with_parameter("foo", 0, 0.0);
         assert_eq!(foo_len(&mut exe), 1);
diff --git a/crates/lib/src/execution_data.rs b/crates/lib/src/execution_data.rs
index fe27355cc..e5f602a37 100644
--- a/crates/lib/src/execution_data.rs
+++ b/crates/lib/src/execution_data.rs
@@ -15,7 +15,7 @@ use crate::RegisterData;
 pub struct Qvm {
     /// The readout data that was read from the [`Executable`](crate::Executable).
     /// Key is the name of the register, value is the data of the register after execution.
-    pub registers: HashMap<Box<str>, RegisterData>,
+    pub registers: HashMap<String, RegisterData>,
     /// The time it took to execute the program on the QPU, not including any network or queueing
     /// time. If paying for on-demand execution, this is the amount you will be billed for.
     ///
@@ -23,12 +23,14 @@ pub struct Qvm {
     pub duration: Option<Duration>,
 }
 
+/// A mapping of readout fields to their [`ReadoutValues`].
 #[derive(Debug, Clone, PartialEq)]
 #[repr(transparent)]
 pub struct ReadoutMap(HashMap<MemoryReference, ReadoutValues>);
 
 impl ReadoutMap {
     /// Given a known readout field name and index, return the result's [`ReadoutValues`], if any.
+    #[must_use]
     pub fn get_readout_values(&self, field: String, index: u64) -> Option<ReadoutValues> {
         let readout_values = self.0.get(&MemoryReference { name: field, index })?;
 
diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs
index 0830541a8..0182bbbca 100644
--- a/crates/lib/src/lib.rs
+++ b/crates/lib/src/lib.rs
@@ -2,16 +2,56 @@
 #![deny(clippy::pedantic)]
 #![deny(clippy::cargo)]
 #![allow(clippy::multiple_crate_versions)] // This should be enforced by cargo-deny
-#![deny(missing_docs)]
 #![allow(clippy::missing_errors_doc)]
 #![forbid(unsafe_code)]
+#![warn(future_incompatible)]
+#![warn(rust_2018_compatibility, rust_2018_idioms)]
+#![deny(
+    absolute_paths_not_starting_with_crate,
+    anonymous_parameters,
+    bad_style,
+    dead_code,
+    deprecated_in_future,
+    keyword_idents,
+    improper_ctypes,
+    let_underscore_drop,
+    macro_use_extern_crate,
+    meta_variable_misuse,
+    missing_abi,
+    missing_copy_implementations,
+    missing_debug_implementations,
+    missing_docs,
+    no_mangle_generic_items,
+    non_shorthand_field_patterns,
+    noop_method_call,
+    overflowing_literals,
+    path_statements,
+    patterns_in_fns_without_body,
+    pointer_structural_match,
+    private_in_public,
+    semicolon_in_expressions_from_macros,
+    trivial_casts,
+    trivial_numeric_casts,
+    unaligned_references,
+    unconditional_recursion,
+    unreachable_pub,
+    unused,
+    unused_allocation,
+    unused_comparisons,
+    unused_extern_crates,
+    unused_import_braces,
+    unused_lifetimes,
+    unused_parens,
+    unused_qualifications,
+    while_true
+)]
 
 //! This crate is the primary Rust API for interacting with Rigetti products. Specifically, this
 //! crate allows you to run Quil programs against real QPUs or a QVM
 //! using [`Executable`].
 
-pub use executable::{Error, Executable, JobHandle, Service};
-pub use execution_data::Qvm;
+pub use executable::{Error, Executable, ExecuteResultQPU, ExecuteResultQVM, JobHandle, Service};
+pub use execution_data::{Qpu, Qvm, ReadoutMap};
 pub use register_data::RegisterData;
 
 pub mod api;
diff --git a/crates/lib/src/qpu/client.rs b/crates/lib/src/qpu/client.rs
index 726d3c762..0b00f93bc 100644
--- a/crates/lib/src/qpu/client.rs
+++ b/crates/lib/src/qpu/client.rs
@@ -26,7 +26,7 @@ pub use qcs_api_client_grpc::channel::Error as GrpcError;
 pub use qcs_api_client_openapi::apis::Error as OpenApiError;
 
 /// A client providing helper functionality for accessing QCS APIs
-#[derive(Clone, Default)]
+#[derive(Debug, Clone, Default)]
 pub struct Qcs {
     config: ClientConfiguration,
     /// When enabled, default to Gateway service for execution. Fallback to QPU's default endpoint otherwise.
@@ -36,7 +36,9 @@ pub struct Qcs {
 impl Qcs {
     /// Create a [`Qcs`] and initialize it with the user's default [`ClientConfiguration`]
     pub async fn load() -> Result<Self, LoadError> {
-        ClientConfiguration::load().await.map(Self::with_config)
+        ClientConfiguration::load_default()
+            .await
+            .map(Self::with_config)
     }
 
     /// Create a [`Qcs`] and initialize it with the given [`ClientConfiguration`]
@@ -124,32 +126,31 @@ impl Qcs {
         &self,
         quantum_processor_id: &str,
     ) -> Result<Uri, GrpcEndpointError> {
-        // TODO: Env override is temporary, to allow for testing. Remove before merge.
-        let gateway_addr = if let Ok(addr) = std::env::var("GATEWAY_ADDR") {
-            addr
-        } else {
+        let mut gateways = Vec::new();
+        let mut next_page_token = None;
+        loop {
             let accessors = list_quantum_processor_accessors(
                 &self.get_openapi_client(),
                 quantum_processor_id,
                 Some(100),
-                None,
+                next_page_token.as_deref(),
             )
             .await?;
-            // TODO: paginate until target found or no more pages
-            let target = accessors
-                .accessors
-                .into_iter()
-                .find(|acc| {
-                    acc.live
-                        && acc.priority.unwrap_or(false)
-                        && acc.access_type.is_some()
-                        && acc.access_type.as_ref().unwrap().as_ref()
-                            == &QuantumProcessorAccessorType::GatewayV1
-                })
-                .ok_or_else(|| GrpcEndpointError::NoEndpoint(quantum_processor_id.into()))?;
-            target.url
-        };
-        parse_uri(&gateway_addr).map_err(GrpcEndpointError::BadUri)
+            gateways.extend(accessors.accessors.into_iter().filter(|acc| {
+                acc.live
+                    // `as_deref` needed to work around the `Option<Box<_>>` type.
+                    && acc.access_type.as_deref() == Some(&QuantumProcessorAccessorType::GatewayV1)
+            }));
+            next_page_token = accessors.next_page_token.clone();
+            if next_page_token.is_none() {
+                break;
+            }
+        }
+        gateways.sort_by_key(|acc| acc.rank);
+        let target = gateways
+            .first()
+            .ok_or_else(|| GrpcEndpointError::NoEndpoint(quantum_processor_id.to_string()))?;
+        parse_uri(&target.url).map_err(GrpcEndpointError::BadUri)
     }
 }
 
diff --git a/crates/lib/src/qpu/execution.rs b/crates/lib/src/qpu/execution.rs
index 1253d5778..00606abfc 100644
--- a/crates/lib/src/qpu/execution.rs
+++ b/crates/lib/src/qpu/execution.rs
@@ -1,5 +1,6 @@
 //! Contains QPU-specific executable stuff.
 
+use std::borrow::Cow;
 use std::collections::HashMap;
 use std::convert::TryFrom;
 use std::sync::Arc;
@@ -16,7 +17,7 @@ use crate::qpu::{rewrite_arithmetic, runner::JobId, translation::translate};
 use crate::JobHandle;
 
 use super::client::{GrpcClientError, Qcs};
-use super::quilc::{self, TargetDevice};
+use super::quilc::{self, CompilerOpts, TargetDevice};
 use super::rewrite_arithmetic::RewrittenProgram;
 use super::runner::{retrieve_results, submit};
 use super::translation::EncryptedTranslationResult;
@@ -25,10 +26,10 @@ use super::{get_isa, IsaError};
 /// Contains all the info needed for a single run of an [`crate::Executable`] against a QPU. Can be
 /// updated with fresh parameters in order to re-run the same program against the same QPU with the
 /// same number of shots.
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub(crate) struct Execution<'a> {
     program: RewrittenProgram,
-    pub(crate) quantum_processor_id: &'a str,
+    pub(crate) quantum_processor_id: Cow<'a, str>,
     pub(crate) shots: u16,
     client: Arc<Qcs>,
 }
@@ -58,10 +59,10 @@ pub(crate) enum Error {
 impl From<quilc::Error> for Error {
     fn from(source: quilc::Error) -> Self {
         match source {
-            quilc::Error::Isa(source) => Self::Unexpected(Unexpected::Isa(format!("{:?}", source))),
+            quilc::Error::Isa(source) => Self::Unexpected(Unexpected::Isa(format!("{source:?}"))),
             quilc::Error::QuilcConnection(uri, details) => Self::Quilc {
                 uri,
-                details: format!("{:?}", details),
+                details: format!("{details:?}"),
             },
             quilc::Error::QuilcCompilation(details) => Self::Compilation { details },
             quilc::Error::Parse(details) => Self::Compilation {
@@ -92,8 +93,11 @@ impl<'a> Execution<'a> {
     /// * `quil`: The raw Quil program to eventually be run on a QPU.
     /// * `shots`: The number of times to run this program with each call to [`Execution::run`].
     /// * `quantum_processor_id`: The QPU this Quil will be run on and should be compiled for.
-    /// * `config`: A [`Configuration`] instance provided by the user which contains connection info
+    /// * `client`: A [`qcs::qpu::client::Qcs`] instance provided by the user which contains connection info
     ///     for QCS and the `quilc` compiler.
+    /// * `compile_with_quilc`: A boolean that if set, will compile the given `quil` using `quilc`
+    /// * `compiler_options`: A [`qcs::qpu::quilc::CompilerOpts`] instance with configuration
+    ///     options for quilc. Has no effect if `compile_with_quilc` is false
     ///
     /// returns: Result<Execution, Report>
     ///
@@ -110,25 +114,27 @@ impl<'a> Execution<'a> {
     pub(crate) async fn new(
         quil: Arc<str>,
         shots: u16,
-        quantum_processor_id: &'a str,
+        quantum_processor_id: Cow<'a, str>,
         client: Arc<Qcs>,
         compile_with_quilc: bool,
+        compiler_options: CompilerOpts,
     ) -> Result<Execution<'a>, Error> {
-        let isa = get_isa(quantum_processor_id, &client).await?;
+        let isa = get_isa(quantum_processor_id.as_ref(), &client).await?;
         let target_device = TargetDevice::try_from(isa)?;
 
         let program = if compile_with_quilc {
             trace!("Converting to Native Quil");
             let client = client.clone();
-            // TODO Support protoquil optional
-            spawn_blocking(move || quilc::compile_program(&quil, target_device, None, &client))
-                .await
-                .map_err(|source| {
-                    Error::Unexpected(Unexpected::TaskError {
-                        task_name: "quilc",
-                        source,
-                    })
-                })??
+            spawn_blocking(move || {
+                quilc::compile_program(&quil, target_device, &client, compiler_options)
+            })
+            .await
+            .map_err(|source| {
+                Error::Unexpected(Unexpected::TaskError {
+                    task_name: "quilc",
+                    source,
+                })
+            })??
         } else {
             trace!("Skipping conversion to Native Quil");
             quil.parse().map_err(Error::Quil)?
@@ -143,9 +149,9 @@ impl<'a> Execution<'a> {
     }
 
     /// Run on a real QPU and wait for the results.
-    pub(crate) async fn submit(&mut self, params: &Parameters) -> Result<JobHandle, Error> {
+    pub(crate) async fn submit(&mut self, params: &Parameters) -> Result<JobHandle<'_>, Error> {
         let EncryptedTranslationResult { job, readout_map } = translate(
-            self.quantum_processor_id,
+            self.quantum_processor_id.as_ref(),
             &self.program.to_string().0,
             self.shots.into(),
             self.client.as_ref(),
@@ -157,7 +163,7 @@ impl<'a> Execution<'a> {
             .map_err(Error::Substitution)?;
 
         let job_id = submit(
-            self.quantum_processor_id,
+            self.quantum_processor_id.as_ref(),
             job,
             &patch_values,
             self.client.as_ref(),
@@ -166,7 +172,7 @@ impl<'a> Execution<'a> {
 
         Ok(JobHandle::new(
             job_id,
-            self.quantum_processor_id,
+            self.quantum_processor_id.clone(),
             readout_map,
         ))
     }
@@ -176,8 +182,12 @@ impl<'a> Execution<'a> {
         job_id: JobId,
         readout_mappings: HashMap<String, String>,
     ) -> Result<Qpu, Error> {
-        let response =
-            retrieve_results(job_id, self.quantum_processor_id, self.client.as_ref()).await?;
+        let response = retrieve_results(
+            job_id,
+            self.quantum_processor_id.as_ref(),
+            self.client.as_ref(),
+        )
+        .await?;
 
         Ok(Qpu {
             readout_data: ReadoutMap::from_mappings_and_values(
diff --git a/crates/lib/src/qpu/mod.rs b/crates/lib/src/qpu/mod.rs
index 452edd537..72df14ebd 100644
--- a/crates/lib/src/qpu/mod.rs
+++ b/crates/lib/src/qpu/mod.rs
@@ -2,8 +2,6 @@
 //! the [`Execution`] struct in this module.
 
 use self::client::OpenApiClientError;
-pub(crate) use client::Qcs;
-pub(crate) use execution::{Error as ExecutionError, Execution};
 use qcs_api_client_openapi::{
     apis::quantum_processors_api::{
         get_instruction_set_architecture, GetInstructionSetArchitectureError,
@@ -19,6 +17,9 @@ pub(crate) mod rpcq;
 pub(crate) mod runner;
 pub(crate) mod translation;
 
+pub use client::Qcs;
+pub(crate) use execution::{Error as ExecutionError, Execution};
+
 /// Query QCS for the ISA of the provided `quantum_processor_id`.
 ///
 /// # Errors
diff --git a/crates/lib/src/qpu/quilc/isa/edge.rs b/crates/lib/src/qpu/quilc/isa/edge.rs
index 98ff1a243..57eff9493 100644
--- a/crates/lib/src/qpu/quilc/isa/edge.rs
+++ b/crates/lib/src/qpu/quilc/isa/edge.rs
@@ -35,9 +35,9 @@ impl Edge {
         }
     }
 
-    pub(crate) fn add_operation<'op_name>(
+    pub(crate) fn add_operation(
         &mut self,
-        op_name: &'op_name str,
+        op_name: &str,
         characteristics: &[Characteristic],
     ) -> Result<(), Error> {
         let operator = match GATE_PARAMS.get_key_value(op_name) {
diff --git a/crates/lib/src/qpu/quilc/isa/mod.rs b/crates/lib/src/qpu/quilc/isa/mod.rs
index df23b3aa7..02049a4ae 100644
--- a/crates/lib/src/qpu/quilc/isa/mod.rs
+++ b/crates/lib/src/qpu/quilc/isa/mod.rs
@@ -15,7 +15,7 @@ mod qubit;
 
 /// Restructuring of an [`InstructionSetArchitecture`] for sending to quilc
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
-pub struct Compiler {
+pub(crate) struct Compiler {
     #[serde(rename = "1Q")]
     qubits: HashMap<String, Qubit>,
     #[serde(rename = "2Q")]
@@ -172,13 +172,13 @@ mod describe_compiler_isa {
             .expect("Could not read Aspen 8 output data");
         let qcs_isa: InstructionSetArchitecture =
             serde_json::from_str(&input).expect("Could not deserialize Aspen-8 input");
-        let expected: serde_json::Value =
+        let expected: Value =
             serde_json::from_str(&expected_json).expect("Could not deserialize Aspen-8 output");
 
         let compiler_isa =
             Compiler::try_from(qcs_isa).expect("Could not convert ISA to CompilerIsa");
         let serialized =
-            serde_json::to_value(&compiler_isa).expect("Unable to serialize CompilerIsa");
+            serde_json::to_value(compiler_isa).expect("Unable to serialize CompilerIsa");
 
         let result = json_is_equivalent(&serialized, &expected);
         result.expect("JSON was not equivalent");
diff --git a/crates/lib/src/qpu/quilc/mod.rs b/crates/lib/src/qpu/quilc/mod.rs
index af3c1b500..26794119b 100644
--- a/crates/lib/src/qpu/quilc/mod.rs
+++ b/crates/lib/src/qpu/quilc/mod.rs
@@ -14,6 +14,9 @@ use super::{rpcq, Qcs};
 
 mod isa;
 
+/// Number of seconds to wait before timing out.
+pub const DEFAULT_COMPILER_TIMEOUT: u8 = 30;
+
 /// Take in a Quil program and produce a "native quil" output from quilc
 ///
 /// # Arguments
@@ -21,6 +24,7 @@ mod isa;
 /// * `program`: The Quil program to compile.
 /// * `isa`: The [`InstructionSetArchitecture`] of the targeted platform. Get this using
 ///     [`super::get_isa`].
+/// * `timeout`: The number of seconds to wait before timing out. If not set, defaults to [`DEFAULT_COMPILER_TIMEOUT`].
 ///
 /// returns: `eyre::Result<quil_rs::Program>`
 ///
@@ -33,20 +37,80 @@ mod isa;
 pub(crate) fn compile_program(
     quil: &str,
     isa: TargetDevice,
-    protoquil: Option<bool>,
     client: &Qcs,
-) -> Result<quil_rs::Program, Error> {
+    options: CompilerOpts,
+) -> Result<Program, Error> {
+    let config = client.get_config();
+    let endpoint = config.quilc_url();
+    let params = QuilcParams::new(quil, isa).with_protoquil(options.protoquil);
+    let request =
+        rpcq::RPCRequest::new("quil_to_native_quil", &params).with_timeout(options.timeout);
+    let rpcq_client = rpcq::Client::new(endpoint)
+        .map_err(|source| Error::from_quilc_error(endpoint.into(), source))?;
+    match rpcq_client.run_request::<_, QuilcCompileProgramResponse>(&request) {
+        Ok(response) => response.quil.parse::<Program>().map_err(Error::Parse),
+        Err(source) => Err(Error::from_quilc_error(endpoint.into(), source)),
+    }
+}
+
+/// A set of options that determine the behavior of compiling programs with quilc
+#[derive(Clone, Copy, Debug)]
+pub struct CompilerOpts {
+    /// The number of seconds to wait before timing out. If `None`, there is no timeout.
+    timeout: Option<u8>,
+
+    /// If the compiler should produce "protoquil" as output. If `None`, the default
+    /// behavior configured in the compiler service is used.
+    protoquil: Option<bool>,
+}
+
+/// Functions for building a [`CompilerOpts`] instance
+impl CompilerOpts {
+    /// Creates a new instance of [`CompilerOpts`] with zero values for each option.
+    /// Consider using [`CompilerOpts::default()`] to create an instance with recommended defaults.
+    #[must_use]
+    pub fn new() -> Self {
+        Self {
+            timeout: None,
+            protoquil: None,
+        }
+    }
+
+    /// Set the number of seconds to wait before timing out. If set to None, the timeout is disabled.
+    #[must_use]
+    pub fn with_timeout(&mut self, seconds: Option<u8>) -> Self {
+        self.timeout = seconds;
+        *self
+    }
+
+    /// Set if the compiler should produce "protoquil" as output.
+    #[must_use]
+    pub fn with_protoquil(&mut self, protoquil: Option<bool>) -> Self {
+        self.protoquil = protoquil;
+        *self
+    }
+}
+
+impl Default for CompilerOpts {
+    /// Default compiler options
+    /// * `timeout`: See [`DEFAULT_COMPILER_TIMEOUT`]
+    fn default() -> Self {
+        Self {
+            timeout: Some(DEFAULT_COMPILER_TIMEOUT),
+            protoquil: None,
+        }
+    }
+}
+
+pub(crate) fn get_version_info(client: &Qcs) -> Result<String, Error> {
     let config = client.get_config();
     let endpoint = config.quilc_url();
-    let params = QuilcParams::new(quil, isa, protoquil);
-    let request = rpcq::RPCRequest::new("quil_to_native_quil", &params);
+    let binding: HashMap<String, String> = HashMap::new();
+    let request = rpcq::RPCRequest::new("get_version_info", &binding);
     let rpcq_client = rpcq::Client::new(endpoint)
         .map_err(|source| Error::from_quilc_error(endpoint.into(), source))?;
-    match rpcq_client.run_request::<_, QuilcResponse>(&request) {
-        Ok(response) => response
-            .quil
-            .parse::<quil_rs::Program>()
-            .map_err(Error::Parse),
+    match rpcq_client.run_request::<_, QuilcVersionResponse>(&request) {
+        Ok(response) => Ok(response.quilc),
         Err(source) => Err(Error::from_quilc_error(endpoint.into(), source)),
     }
 }
@@ -78,8 +142,13 @@ impl Error {
 }
 
 #[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
-struct QuilcResponse {
-    pub quil: String,
+struct QuilcCompileProgramResponse {
+    quil: String,
+}
+
+#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
+struct QuilcVersionResponse {
+    quilc: String,
 }
 
 /// The top level params that get passed to quilc
@@ -91,12 +160,16 @@ struct QuilcParams {
 }
 
 impl QuilcParams {
-    fn new(quil: &str, isa: TargetDevice, protoquil: Option<bool>) -> Self {
+    fn new(quil: &str, isa: TargetDevice) -> Self {
         Self {
-            protoquil,
+            protoquil: None,
             args: [NativeQuilRequest::new(quil, isa)],
         }
     }
+
+    fn with_protoquil(self, protoquil: Option<bool>) -> Self {
+        Self { protoquil, ..self }
+    }
 }
 
 /// The expected request structure for sending Quil to quilc to be compiled
@@ -139,7 +212,8 @@ impl TryFrom<InstructionSetArchitecture> for TargetDevice {
 mod tests {
     use super::*;
     use qcs_api_client_openapi::models::InstructionSetArchitecture;
-    use std::fs::File;
+    use regex::Regex;
+    use std::{borrow::Cow, fs::File};
 
     const EXPECTED_H0_OUTPUT: &str = "MEASURE 0\n";
 
@@ -147,7 +221,7 @@ mod tests {
         serde_json::from_reader(File::open("tests/aspen_9_isa.json").unwrap()).unwrap()
     }
 
-    pub fn qvm_isa() -> InstructionSetArchitecture {
+    pub(crate) fn qvm_isa() -> InstructionSetArchitecture {
         serde_json::from_reader(File::open("tests/qvm_isa.json").unwrap()).unwrap()
     }
 
@@ -156,8 +230,8 @@ mod tests {
         let output = compile_program(
             "MEASURE 0",
             TargetDevice::try_from(qvm_isa()).expect("Couldn't build target device from ISA"),
-            None,
             &Qcs::load().await.unwrap_or_default(),
+            CompilerOpts::default(),
         )
         .expect("Could not compile");
         assert_eq!(output.to_string(true), EXPECTED_H0_OUTPUT);
@@ -178,13 +252,18 @@ MEASURE 1 ro[1]
         let output = compile_program(
             BELL_STATE,
             TargetDevice::try_from(aspen_9_isa()).expect("Couldn't build target device from ISA"),
-            None,
             &client,
+            CompilerOpts::default(),
         )
         .expect("Could not compile");
         let mut results = crate::qvm::Execution::new(&output.to_string(true))
             .unwrap()
-            .run(10, &["ro"], &HashMap::default(), &client.get_config())
+            .run(
+                10,
+                &[Cow::Borrowed("ro")],
+                &HashMap::default(),
+                &client.get_config(),
+            )
             .await
             .expect("Could not run program on QVM");
         for shot in results
@@ -197,4 +276,12 @@ MEASURE 1 ro[1]
             assert_eq!(shot[0], shot[1]);
         }
     }
+
+    #[tokio::test]
+    async fn get_version_info_from_quilc() {
+        let client = Qcs::load().await.unwrap_or_default();
+        let version = get_version_info(&client).expect("Should get version info from quilc");
+        let semver_re = Regex::new(r"^([0-9]+)\.([0-9]+)\.([0-9]+)$").unwrap();
+        assert!(semver_re.is_match(&version));
+    }
 }
diff --git a/crates/lib/src/qpu/rewrite_arithmetic.rs b/crates/lib/src/qpu/rewrite_arithmetic.rs
index 7d4626d0b..4e20fb619 100644
--- a/crates/lib/src/qpu/rewrite_arithmetic.rs
+++ b/crates/lib/src/qpu/rewrite_arithmetic.rs
@@ -129,7 +129,7 @@ pub(crate) fn get_substitutions(
         .map(|substitution: &Expression| {
             substitution
                 .evaluate(&HashMap::new(), &params)
-                .map_err(|e| format!("Could not evaluate expression {}: {:?}", substitution, e))
+                .map_err(|e| format!("Could not evaluate expression {substitution}: {e:?}"))
                 .and_then(|complex| {
                     if complex.im == 0.0 {
                         Ok(complex.re)
@@ -329,7 +329,7 @@ pub(crate) struct RewrittenProgram {
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
-pub(crate) struct RewrittenQuil(pub String);
+pub(crate) struct RewrittenQuil(pub(crate) String);
 
 impl From<RewrittenQuil> for String {
     fn from(quil: RewrittenQuil) -> String {
@@ -337,10 +337,10 @@ impl From<RewrittenQuil> for String {
     }
 }
 
-impl TryFrom<quil_rs::Program> for RewrittenProgram {
+impl TryFrom<Program> for RewrittenProgram {
     type Error = Error;
 
-    fn try_from(program: quil_rs::Program) -> Result<Self, Self::Error> {
+    fn try_from(program: Program) -> Result<Self, Self::Error> {
         let (inner, substitutions) = rewrite_arithmetic(program)?;
         Ok(Self {
             inner,
diff --git a/crates/lib/src/qpu/rpcq.rs b/crates/lib/src/qpu/rpcq.rs
index ce8276c7f..77068fb17 100644
--- a/crates/lib/src/qpu/rpcq.rs
+++ b/crates/lib/src/qpu/rpcq.rs
@@ -11,6 +11,8 @@ use zmq::{Context, Socket, SocketType};
 
 use crate::qpu::rpcq::Error::AuthSetup;
 
+pub(crate) const DEFAULT_CLIENT_TIMEOUT: u8 = 30;
+
 /// A minimal RPCQ client that does just enough to talk to `quilc` and QPU endpoints
 pub(crate) struct Client {
     socket: Socket,
@@ -27,7 +29,7 @@ impl Client {
     }
 
     /// Construct a new [`Client`] with authentication.
-    fn new_with_credentials(endpoint: &str, credentials: &Credentials) -> Result<Self, Error> {
+    fn new_with_credentials(endpoint: &str, credentials: &Credentials<'_>) -> Result<Self, Error> {
         let socket = Context::new()
             .socket(SocketType::DEALER)
             .map_err(Error::SocketCreation)?;
@@ -51,7 +53,7 @@ impl Client {
     /// * `request`: An [`RPCRequest`] containing some params.
     pub(crate) fn run_request<Request: Serialize, Response: DeserializeOwned>(
         &self,
-        request: &RPCRequest<Request>,
+        request: &RPCRequest<'_, Request>,
     ) -> Result<Response, Error> {
         self.send(request)?;
         self.receive::<Response>(&request.id)
@@ -64,7 +66,7 @@ impl Client {
     /// * `request`: An [`RPCRequest`] containing some params.
     pub(crate) fn send<Request: Serialize>(
         &self,
-        request: &RPCRequest<Request>,
+        request: &RPCRequest<'_, Request>,
     ) -> Result<(), Error> {
         let mut data = vec![];
         request
@@ -157,7 +159,7 @@ where
     params: &'params T,
     id: String,
     jsonrpc: &'static str,
-    client_timeout: u8,
+    client_timeout: Option<u8>,
     client_key: Option<String>,
 }
 
@@ -170,24 +172,36 @@ impl<'params, T: Serialize> RPCRequest<'params, T> {
     /// * `params`: The parameters to send. This must implement [`serde::Serialize`].
     ///
     /// returns: `RPCRequest<T>` where `T` is the type you passed in as `params`.
-    pub fn new(method: &'static str, params: &'params T) -> Self {
+    pub(crate) fn new(method: &'static str, params: &'params T) -> Self {
         Self {
             method,
             params,
             id: Uuid::new_v4().to_string(),
             jsonrpc: "2.0",
-            client_timeout: 10,
+            client_timeout: Some(DEFAULT_CLIENT_TIMEOUT),
             client_key: None,
         }
     }
+
+    /// Sets the client timeout for the [`RPCRequest`].
+    ///
+    /// # Arguments
+    ///
+    /// * `seconds`: The number of seconds to wait before timing out, or None for no timeout
+    ///
+    /// returns: `RPCRequest<T>` with the updated timeout.
+    pub(crate) fn with_timeout(mut self, seconds: Option<u8>) -> Self {
+        self.client_timeout = seconds;
+        self
+    }
 }
 
 /// Credentials for connecting to RPCQ Server
 #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
 struct Credentials<'a> {
-    pub client_secret_key: &'a str,
-    pub client_public_key: &'a str,
-    pub server_public_key: &'a str,
+    client_secret_key: &'a str,
+    client_public_key: &'a str,
+    server_public_key: &'a str,
 }
 
 #[derive(Deserialize, Debug)]
diff --git a/crates/lib/src/qpu/runner.rs b/crates/lib/src/qpu/runner.rs
index fed18d0c5..ab6b5048e 100644
--- a/crates/lib/src/qpu/runner.rs
+++ b/crates/lib/src/qpu/runner.rs
@@ -43,7 +43,7 @@ pub(crate) fn params_into_job_execution_configuration(
 
 /// The QCS Job ID. Useful for debugging or retrieving results later.
 #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
-pub struct JobId(pub(crate) String);
+pub(crate) struct JobId(pub(crate) String);
 
 /// Execute compiled program on a QPU.
 pub(crate) async fn submit(
@@ -132,15 +132,15 @@ impl Buffer {
 /// Generally this should not be used directly, but converted into an appropriate
 /// 2-D array.
 #[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Eq)]
-pub struct Buffer {
-    pub shape: Vec<usize>,
-    pub data: ByteBuf,
-    pub dtype: DataType,
+struct Buffer {
+    shape: Vec<usize>,
+    data: ByteBuf,
+    dtype: DataType,
 }
 
 #[derive(Deserialize, Serialize, Debug, Copy, Clone, PartialEq, Eq)]
 #[serde(rename_all = "lowercase")]
-pub enum DataType {
+enum DataType {
     Float64,
     Int16,
     Complex64,
diff --git a/crates/lib/src/qvm/execution.rs b/crates/lib/src/qvm/execution.rs
index 2f3715cad..a2e4182c7 100644
--- a/crates/lib/src/qvm/execution.rs
+++ b/crates/lib/src/qvm/execution.rs
@@ -1,5 +1,5 @@
-use std::collections::HashMap;
 use std::str::FromStr;
+use std::{borrow::Cow, collections::HashMap};
 
 use qcs_api_client_common::ClientConfiguration;
 use quil_rs::{
@@ -58,10 +58,10 @@ impl Execution {
     pub(crate) async fn run(
         &mut self,
         shots: u16,
-        readouts: &[&str],
+        readouts: &[Cow<'_, str>],
         params: &Parameters,
         config: &ClientConfiguration,
-    ) -> Result<HashMap<Box<str>, RegisterData>, Error> {
+    ) -> Result<HashMap<String, RegisterData>, Error> {
         if shots == 0 {
             return Err(Error::ShotsMustBePositive);
         }
@@ -108,9 +108,9 @@ impl Execution {
     async fn execute(
         &self,
         shots: u16,
-        readouts: &[&str],
+        readouts: &[Cow<'_, str>],
         config: &ClientConfiguration,
-    ) -> Result<HashMap<Box<str>, RegisterData>, Error> {
+    ) -> Result<HashMap<String, RegisterData>, Error> {
         let request = Request::new(&self.program.to_string(true), shots, readouts);
 
         let client = reqwest::Client::new();
@@ -129,11 +129,7 @@ impl Execution {
                 qvm_url: config.qvm_url().into(),
                 source,
             }),
-            Ok(Response::Success(response)) => Ok(response
-                .registers
-                .into_iter()
-                .map(|(key, value)| (key.into_boxed_str(), value))
-                .collect()),
+            Ok(Response::Success(response)) => Ok(response.registers),
             Ok(Response::Failure(response)) => Err(Error::Qvm {
                 message: response.status,
             }),
diff --git a/crates/lib/src/qvm/mod.rs b/crates/lib/src/qvm/mod.rs
index 788291b8d..33dd3f8c9 100644
--- a/crates/lib/src/qvm/mod.rs
+++ b/crates/lib/src/qvm/mod.rs
@@ -1,7 +1,7 @@
 //! This module contains all the functionality for running Quil programs on a QVM. Specifically,
 //! the [`Execution`] struct in this module.
 
-use std::collections::HashMap;
+use std::{borrow::Cow, collections::HashMap};
 
 use serde::{Deserialize, Serialize};
 
@@ -21,13 +21,13 @@ pub(super) enum Response {
 #[derive(Debug, Deserialize, Clone, PartialEq)]
 pub(super) struct Success {
     #[serde(flatten)]
-    pub registers: HashMap<String, RegisterData>,
+    pub(super) registers: HashMap<String, RegisterData>,
 }
 
 #[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
 pub(super) struct Failure {
     /// The message from QVM describing what went wrong.
-    pub status: String,
+    pub(super) status: String,
 }
 
 #[derive(Serialize, Debug, Clone, PartialEq, Eq)]
@@ -41,8 +41,8 @@ struct Request<'request> {
 }
 
 impl<'request> Request<'request> {
-    fn new(program: &str, shots: u16, readouts: &[&'request str]) -> Self {
-        let addresses: HashMap<&str, bool> = readouts.iter().map(|v| (*v, true)).collect();
+    fn new(program: &str, shots: u16, readouts: &'request [Cow<'request, str>]) -> Self {
+        let addresses: HashMap<&str, bool> = readouts.iter().map(|v| (v.as_ref(), true)).collect();
         Self {
             quil_instructions: program.to_string(),
             addresses,
@@ -60,6 +60,8 @@ enum RequestType {
 
 #[cfg(test)]
 mod describe_request {
+    use std::borrow::Cow;
+
     use super::Request;
 
     #[test]
@@ -71,7 +73,7 @@ mod describe_request {
 
     #[test]
     fn it_uses_kebab_case_for_json() {
-        let request = Request::new("H 0", 10, &["ro"]);
+        let request = Request::new("H 0", 10, &[Cow::Borrowed("ro")]);
         let json_string = serde_json::to_string(&request).expect("Could not serialize QVMRequest");
         assert_eq!(
             serde_json::from_str::<serde_json::Value>(&json_string).unwrap(),
diff --git a/crates/python/CHANGELOG.md b/crates/python/CHANGELOG.md
index 08e9d25db..14b52a8d7 100644
--- a/crates/python/CHANGELOG.md
+++ b/crates/python/CHANGELOG.md
@@ -1,3 +1,63 @@
+## 0.5.0-rc.1
+
+### Breaking Changes
+
+- implement Python wrappers for the full Rust API (#230)
+
+## 0.5.0-rc.0
+
+### Breaking Changes
+
+- implement Python wrappers for the full Rust API (#230)
+
+## 0.4.2
+
+### Fixes
+
+- bump quil-rs to fix waveform issues (#228)
+
+## 0.4.2-rc.0
+
+### Fixes
+
+- bump quil-rs to fix waveform issues (#228)
+
+## 0.4.1
+
+### Fixes
+
+- Fetch Gateway address with pagination (#225)
+
+## 0.4.1-rc.0
+
+### Fixes
+
+- Fetch Gateway address with pagination (#225)
+
+## 0.4.0
+
+### Breaking Changes
+
+- compiler timeout is now configurable
+
+## 0.4.0-rc.0
+
+### Breaking Changes
+
+- compiler timeout is now configurable
+
+## 0.3.4
+
+### Features
+
+- add method for getting the version of the running quilc server (#206)
+
+## 0.3.4-rc.0
+
+### Features
+
+- add method for getting the version of the running quilc server (#206)
+
 ## 0.3.3
 
 ### Fixes
diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml
index 3bbc017f8..80d28a539 100644
--- a/crates/python/Cargo.toml
+++ b/crates/python/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "qcs-sdk-python"
 description = "Python bindings to qcs-sdk-rust"
-version = "0.3.3"
+version = "0.5.0-rc.1"
 edition = "2021"
 license = "Apache-2.0"
 repository = "https://github.com/rigetti/qcs-sdk-rust"
@@ -17,13 +17,14 @@ crate-type = ["cdylib"]
 
 [dependencies]
 qcs = { path = "../lib" }
-qcs-api-client-common = "0.2.6"
+qcs-api-client-common = "0.3.0"
+qcs-api-client-grpc = "0.3.0"
 pyo3 = { version = "0.17", features = ["extension-module"] }
 pyo3-asyncio = { version = "0.17", features = ["tokio-runtime"] }
-pythonize = "0.17"
-quil-rs = "0.14"
+quil-rs = "0.15"
 tokio = "1.21"
 qcs-api = "0.2.1"
+rigetti-pyo3 = { version = "0.1.0-rc.1", features = ["extension-module", "complex"] }
 serde_json = "1.0.86"
 
 [build-dependencies]
diff --git a/crates/python/poetry.lock b/crates/python/poetry.lock
index 388b8c467..e92cf03ec 100644
--- a/crates/python/poetry.lock
+++ b/crates/python/poetry.lock
@@ -1,24 +1,26 @@
 [[package]]
 name = "attrs"
-version = "22.1.0"
+version = "22.2.0"
 description = "Classes Without Boilerplate"
 category = "dev"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
-docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
+cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
+dev = ["attrs"]
+docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
+tests = ["attrs", "zope.interface"]
+tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"]
+tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"]
 
 [[package]]
 name = "black"
-version = "22.8.0"
+version = "22.12.0"
 description = "The uncompromising code formatter."
 category = "dev"
 optional = false
-python-versions = ">=3.6.2"
+python-versions = ">=3.7"
 
 [package.dependencies]
 click = ">=8.0.0"
@@ -26,7 +28,6 @@ mypy-extensions = ">=0.4.3"
 pathspec = ">=0.9.0"
 platformdirs = ">=2"
 tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
-typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
 typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
 
 [package.extras]
@@ -45,44 +46,37 @@ python-versions = ">=3.7"
 
 [package.dependencies]
 colorama = {version = "*", markers = "platform_system == \"Windows\""}
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 
 [[package]]
 name = "colorama"
-version = "0.4.5"
+version = "0.4.6"
 description = "Cross-platform colored terminal text."
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 
 [[package]]
-name = "importlib-metadata"
-version = "4.12.0"
-description = "Read metadata from Python packages"
+name = "exceptiongroup"
+version = "1.1.0"
+description = "Backport of PEP 654 (exception groups)"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 
-[package.dependencies]
-typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
-zipp = ">=0.5"
-
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
-perf = ["ipython"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
+test = ["pytest (>=6)"]
 
 [[package]]
 name = "iniconfig"
-version = "1.1.1"
-description = "iniconfig: brain-dead simple config-ini parsing"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.7"
 
 [[package]]
 name = "maturin"
-version = "0.13.3"
+version = "0.13.7"
 description = "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages"
 category = "dev"
 optional = false
@@ -105,18 +99,15 @@ python-versions = "*"
 
 [[package]]
 name = "packaging"
-version = "21.3"
+version = "23.0"
 description = "Core utilities for Python packages"
 category = "dev"
 optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+python-versions = ">=3.7"
 
 [[package]]
 name = "pathspec"
-version = "0.10.1"
+version = "0.10.3"
 description = "Utility library for gitignore style pattern matching of file paths."
 category = "dev"
 optional = false
@@ -124,15 +115,15 @@ python-versions = ">=3.7"
 
 [[package]]
 name = "platformdirs"
-version = "2.5.2"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+version = "2.6.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 
 [package.extras]
-docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
-test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
+docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx (>=5.3)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"]
 
 [[package]]
 name = "pluggy"
@@ -142,35 +133,13 @@ category = "dev"
 optional = false
 python-versions = ">=3.6"
 
-[package.dependencies]
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-
 [package.extras]
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
-[[package]]
-name = "py"
-version = "1.11.0"
-description = "library with cross-python path, ini-parsing, io, code, log facilities"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[[package]]
-name = "pyparsing"
-version = "3.0.9"
-description = "pyparsing module - Classes and methods to define and execute parsing grammars"
-category = "dev"
-optional = false
-python-versions = ">=3.6.8"
-
-[package.extras]
-diagrams = ["railroad-diagrams", "jinja2"]
-
 [[package]]
 name = "pytest"
-version = "7.1.3"
+version = "7.2.1"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
@@ -179,12 +148,11 @@ python-versions = ">=3.7"
 [package.dependencies]
 attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
 iniconfig = "*"
 packaging = "*"
 pluggy = ">=0.12,<2.0"
-py = ">=1.8.2"
-tomli = ">=1.0.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
 
 [package.extras]
 testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
@@ -199,7 +167,6 @@ python-versions = ">=3.7"
 
 [package.dependencies]
 pytest = ">=6.1.0"
-typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
 
 [package.extras]
 testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
@@ -212,38 +179,18 @@ category = "dev"
 optional = false
 python-versions = ">=3.7"
 
-[[package]]
-name = "typed-ast"
-version = "1.5.4"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
 [[package]]
 name = "typing-extensions"
-version = "4.3.0"
+version = "4.4.0"
 description = "Backported and Experimental Type Hints for Python 3.7+"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 
-[[package]]
-name = "zipp"
-version = "3.8.1"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
-
 [metadata]
 lock-version = "1.1"
-python-versions = "^3.7"
-content-hash = "d974c959893832d70b661e1805ddc82b886d828edd542b944f11512e6f2a3914"
+python-versions = "^3.8"
+content-hash = "67c0952caa1ebf2881aec378850a3188a6650b6b9cc9cda838dec5d23ed15e72"
 
 [metadata.files]
 attrs = []
@@ -253,68 +200,24 @@ click = [
     {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
 ]
 colorama = []
-importlib-metadata = []
-iniconfig = [
-    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
-    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
-]
+exceptiongroup = []
+iniconfig = []
 maturin = []
 mypy-extensions = [
     {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
 ]
-packaging = [
-    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
-    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
-]
+packaging = []
 pathspec = []
-platformdirs = [
-    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
-    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
-]
+platformdirs = []
 pluggy = [
     {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
     {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
 ]
-py = [
-    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
-    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
-]
-pyparsing = [
-    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
-    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
-]
 pytest = []
 pytest-asyncio = []
 tomli = [
     {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
 ]
-typed-ast = [
-    {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
-    {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
-    {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
-    {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
-    {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
-    {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
-    {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
-    {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
-    {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
-    {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
-    {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
-    {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
-    {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
-    {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
-    {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
-    {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
-]
 typing-extensions = []
-zipp = []
diff --git a/crates/python/pyproject.toml b/crates/python/pyproject.toml
index 4f02ff4a7..5bfd77313 100644
--- a/crates/python/pyproject.toml
+++ b/crates/python/pyproject.toml
@@ -1,7 +1,7 @@
 # This is the metadata Maturin uploads to PyPI on publish
 [project]
 name = "qcs-sdk-python"
-version = "0.3.3"
+version = "0.5.0-rc.1"
 description = "Python interface for the QCS Rust SDK"
 readme = "README.md"
 license = { text = "Apache-2.0" }
@@ -24,7 +24,7 @@ classifiers = [
 # We can remove this table once this issue is resolved: https://github.com/python-poetry/poetry/issues/3332
 [tool.poetry]
 name = "qcs-sdk-python"
-version = "0.3.3"
+version = "0.5.0-rc.1"
 description = "Python interface for the QCS Rust SDK"
 readme = "README.md"
 authors = [
@@ -40,7 +40,7 @@ compatibility = "linux"
 sdist-include = ["README.md"]
 
 [tool.poetry.dependencies]
-python = "^3.7"
+python = "^3.8"
 
 [tool.poetry.dev-dependencies]
 maturin = "^0.13.2"
diff --git a/crates/python/qcs_sdk.pyi b/crates/python/qcs_sdk.pyi
deleted file mode 100644
index 7da16277a..000000000
--- a/crates/python/qcs_sdk.pyi
+++ /dev/null
@@ -1,159 +0,0 @@
-"""
-The qcs_sdk module provides an interface to Rigetti Quantum Cloud Services. Allowing users to compile and run Quil programs on Rigetti quantum processors.
-"""
-from typing import Any, Dict, List, Optional, TypedDict
-from numbers import Number
-
-RecalculationTable = List[str]
-Memory = Dict[str, List[float]]
-PatchValues = Dict[str, List[float]]
-
-class RewriteArithmeticResults(TypedDict):
-    program: str
-    """
-    The resulting program where gate parameter arithmetic has been replaced with memory references. Before execution, the program memory should be updated using the `recalculation_table`.
-    """
-
-    recalculation_table: RecalculationTable
-    """ 
-    The recalculation table stores an ordered list of arithmetic expressions, which are to be used when updating the program memory before execution.
-    """
-
-class TranslationResult(TypedDict):
-    memory_descriptors: Optional[Dict[str, Any]]
-    """
-    A map from the name of memory (declared with `DECLARE`) to the size and type of that memory.
-    """
-
-    program: str
-    """
-    The compiled program binary.
-    """
-
-    ro_sources: Optional[List[List[str]]]
-    """
-    A mapping from the program's memory references to the key used to index the results map.
-    """
-
-    settings_timestamp: Optional[str]
-    """
-    The timestamp of the settings used during translation.
-    """
-
-class ExecutionResult(TypedDict):
-    shape: List[int]
-    """
-    The shape of the result data.
-    """
-
-    data: List[Number | List[float]]
-    """
-    The result data. Complex numbers are represented as [real, imaginary].
-    """
-
-    dtype: str
-    """
-    The type of the result data (as a `numpy` `dtype`).
-    """
-
-class ExecutionResults(TypedDict):
-    buffers: Dict[str, ExecutionResult]
-    """
-    The readout results of execution, mapping a published filter node to its data.
-
-    See `TranslationResult.ro_sources` which provides the mapping from the filter node name to the name of the memory declaration in the source program.
-    """
-
-    execution_duration_microseconds: Optional[int]
-    """
-    The time spent executing the program.
-    """
-
-async def compile(quil: str, target_device: str) -> str:
-    """
-    Uses quilc to convert a quil program to native Quil.
-
-    Args:
-        quil: A Quil program.
-        target_device: A JSON encoded description of the Quantum Abstract Machine Architecture.
-
-    Returns:
-        An Awaitable that resolves to the native Quil program.
-    """
-    ...
-
-def rewrite_arithmetic(native_quil: str) -> RewriteArithmeticResults:
-    """
-    Rewrites parametric arithmetic such that all gate parameters are only memory references
-    to a newly declared memory location (__SUBST).
-
-    Args:
-        native_quil: A Quil program.
-
-    Returns:
-        A dictionary with the rewritten program and recalculation table (see `RewriteArithmeticResults`).
-    """
-    ...
-
-def build_patch_values(
-    recalculation_table: RecalculationTable, memory: Memory
-) -> PatchValues:
-    """
-    Evaluate the expressions in recalculation_table using the numeric values
-    provided in memory.
-
-    Args:
-        recalculation_table: an ordered list of arithmetic expressions, which are to be used when updating the program memory before execution (see `rewrite_arithmetic`).
-        memory: A mapping of symbols to their values (e.g. `{"theta": [0.0]}`).
-
-    Returns:
-        A dictionary that maps each symbol to the value it should be patched with.
-    """
-    ...
-
-async def translate(
-    native_quil: str, num_shots: int, quantum_processor_id: str
-) -> TranslationResult:
-    """
-    Translates a native Quil program into an executable.
-
-    Args:
-        native_quil: A Quil program.
-        num_shots: The number of shots to perform.
-        quantum_processor_id: The ID of the quantum processor the executable will run on (e.g. "Aspen-M-2").
-
-    Returns:
-        An Awaitable that resolves to a dictionary with the compiled program, memory descriptors, and readout sources (see `TranslationResult`).
-    """
-    ...
-
-async def submit(
-    program: str, patch_values: Dict[str, List[float]], quantum_processor_id: str
-) -> str:
-    """
-    Submits an executable `program` to be run on the specified QPU.
-
-    Args:
-        program: An executable program (see `translate`).
-        patch_values: A mapping of symbols to their desired values (see `build_patch_values`).
-        quantum_processor_id: The ID of the quantum processor to run the executable on.
-
-    Returns:
-        An Awaitable that resolves to the ID of the submitted job.
-    """
-    ...
-
-async def retrieve_results(
-    job_id: str, quantum_processor_id: str
-) -> ExecutionResults:
-    """
-    Fetches results for the corresponding job ID.
-
-    Args:
-        job_id: The ID of the job to retrieve results for.
-        quantum_processor_id: The ID of the quanutum processor the job ran on.
-
-    Returns:
-        An Awaitable that resolves to a dictionary describing the results of the execution and its duration (see `ExecutionResults`).
-    """
-    ...
diff --git a/crates/python/qcs_sdk/__init__.py b/crates/python/qcs_sdk/__init__.py
new file mode 100644
index 000000000..e0d6434d7
--- /dev/null
+++ b/crates/python/qcs_sdk/__init__.py
@@ -0,0 +1,4 @@
+# See the following documentation for why this file is necessary:
+# https://pyo3.rs/v0.18.0/python_typing_hints#__init__py-content
+
+from .qcs_sdk import *
diff --git a/crates/python/qcs_sdk/__init__.pyi b/crates/python/qcs_sdk/__init__.pyi
new file mode 100644
index 000000000..279ad5cb6
--- /dev/null
+++ b/crates/python/qcs_sdk/__init__.pyi
@@ -0,0 +1,23 @@
+from .api import *
+
+from .qpu.client import (
+    QcsClient as QcsClient
+)
+
+from ._execution_data import (
+    QPU as QPU,
+    QVM as QVM,
+    ReadoutMap as ReadoutMap,
+)
+
+from ._executable import (
+    Executable as Executable,
+    ExeParameter as ExeParameter,
+    JobHandle as JobHandle,
+    QcsExecutionError as QcsExecutionError,
+    Service as Service,
+)
+
+from ._register_data import (
+    RegisterData as RegisterData,
+)
diff --git a/crates/python/qcs_sdk/_executable.pyi b/crates/python/qcs_sdk/_executable.pyi
new file mode 100644
index 000000000..2ec80827d
--- /dev/null
+++ b/crates/python/qcs_sdk/_executable.pyi
@@ -0,0 +1,109 @@
+"""
+Do not import this file, it has no exports.
+It is only here to represent the structure of the rust source code 1:1
+"""
+from enum import Enum
+
+from typing import Dict, List, Optional
+from .qpu.quilc import CompilerOpts
+from ._execution_data import QVM, QPU
+
+class QcsExecutionError(RuntimeError):
+    """Error encounteted when executing programs."""
+    ...
+
+
+class Executable:
+    def __new__(
+        cls,
+        registers: Optional[List[str]] = None,
+        parameters: Optional[List[ExeParameter]] = None,
+        shots: Optional[int] = None,
+        compile_with_quilc: Optional[bool] = None,
+        compiler_options: Optional[CompilerOpts] = None
+    ) -> "Executable": ...
+
+    async def execute_on_qvm(self) -> QVM:
+        """
+        Execute on a QVM which must be available at the configured URL (default http://localhost:5000).
+
+        Raises:
+            - ``QcsExecutionError``: If the job fails to execute.
+        """
+        ...
+
+    async def execute_on_qpu(self, quantum_processor_id: str) -> QPU:
+        """
+        Compile the program and execute it on a QPU, waiting for results.
+
+        Raises:
+            - ``QcsExecutionError``: If the job fails to execute.
+        """
+        ...
+    
+    async def retrieve_results(job_handle: JobHandle) -> QPU:
+        """
+        Wait for the results of a job to complete.
+
+        Raises:
+            - ``QcsExecutionError``: If there is a problem constructing job results.
+        """
+        ...
+
+
+class JobHandle:
+    """
+    The result of submitting a job to a QPU.
+    
+    Used to retrieve the results of a job.
+    """
+
+    @property
+    def job_id(self) -> str:
+        """
+        Unique ID associated with a single job execution.
+        """
+        ...
+
+    @property
+    def readout_map(self) -> Dict[str, str]:
+        """
+        The readout map from source readout memory locations to the filter pipeline node which publishes the data.
+        """
+        ...
+
+
+class ExeParameter:
+    """
+    Program execution parameters.
+
+    Note: The validity of parameters is not checked until execution.
+    """
+    def __new__(
+        cls: type["ExeParameter"],
+        name: str,
+        index: int,
+        value: float,
+    ) -> "ExeParameter": ...
+
+    @property
+    def name(self) -> str: ...
+    @name.setter
+    def name(self, value: str): ...
+
+    @property
+    def index(self) -> int: ...
+    @index.setter
+    def index(self, value: int): ...
+
+    @property
+    def value(self) -> float: ...
+    @value.setter
+    def value(self, value: float): ...
+
+
+class Service(Enum):
+    Quilc = "Quilc",
+    Qvm = "Qvm",
+    Qcs = "Qcs",
+    Qpu = "Qpu",
diff --git a/crates/python/qcs_sdk/_execution_data.pyi b/crates/python/qcs_sdk/_execution_data.pyi
new file mode 100644
index 000000000..d64ae413f
--- /dev/null
+++ b/crates/python/qcs_sdk/_execution_data.pyi
@@ -0,0 +1,72 @@
+"""
+Do not import this file, it has no exports.
+It is only here to represent the structure of the rust source code 1:1
+"""
+
+import datetime
+from typing import List, Optional
+
+class _IntegerReadoutValues:
+    @property
+    def values(self) -> List[int]: ...
+    @values.setter
+    def values(self, value: List[int]): ...
+
+
+class _ComplexReadoutValues:
+    @property
+    def values(self) -> List[complex]: ...
+    @values.setter
+    def values(self, value: List[complex]): ...
+
+
+class _ReadoutValuesValues:
+    @property
+    def integer_values(self) -> _IntegerReadoutValues: ...
+    @integer_values.setter
+    def integer_values(self, value: _IntegerReadoutValues): ...
+
+    @property
+    def complex_values(self) -> _ComplexReadoutValues: ...
+    @complex_values.setter
+    def complex_values(self, value: _ComplexReadoutValues): ...
+
+
+class _ReadoutValues:
+    @property
+    def values(self) -> Optional[_ReadoutValuesValues]: ...
+    @values.setter
+    def values(self, value: Optional[_ReadoutValuesValues]): ...
+
+
+class ReadoutMap:
+    def get_readout_values(self, field: str, index: int) -> Optional[_ReadoutValues]:
+        """Given a known readout field name and index, return the result's ``ReadoutValues``, if any."""
+        ...
+    
+    def get_readout_values_for_field(self, field: str) -> Optional[List[Optional[_ReadoutValues]]]:
+        """Given a known readout field name, return the result's ``ReadoutValues`` for all indices, if any."""
+        ...
+
+class QVM:
+    @property
+    def registers(self) -> dict: ...
+    @registers.setter
+    def registers(self, value: dict): ...
+
+    @property
+    def duration(self) -> Optional[datetime.timedelta]: ...
+    @duration.setter
+    def duration(self, value: Optional[datetime.timedelta]): ...
+
+
+class QPU:
+    @property
+    def readout_data(self) -> ReadoutMap: ...
+    @readout_data.setter
+    def readout_data(self, value: ReadoutMap): ...
+
+    @property
+    def duration(self) -> Optional[datetime.timedelta]: ...
+    @duration.setter
+    def duration(self, value: Optional[datetime.timedelta]): ...
diff --git a/crates/python/qcs_sdk/_register_data.pyi b/crates/python/qcs_sdk/_register_data.pyi
new file mode 100644
index 000000000..3e7bd7313
--- /dev/null
+++ b/crates/python/qcs_sdk/_register_data.pyi
@@ -0,0 +1,49 @@
+"""
+Do not import this file, it has no exports.
+It is only here to represent the structure of the rust source code 1:1
+"""
+
+from typing import List, Optional
+
+
+class RegisterData:
+    """
+    Values present in a register that are one of a set of variants.
+
+    Variants:
+        - ``i8``: Corresponds to the Quil `BIT` or `OCTET` types.
+        - ``i16``: Corresponds to the Quil `INTEGER` type.
+        - ``f64``: Corresponds to the Quil `REAL` type.
+        - ``complex32``: Results containing complex numbers.
+
+    Methods (each per variant):
+        - ``is_*``: if the underlying values are that type.
+        - ``as_*``: if the underlying values are that type, then those values, otherwise ``None``.
+        - ``to_*``: the underlying values as that type, raises ``ValueError`` if they are not.
+        - ``from_*``: wrap underlying values as this enum type.
+
+    """
+    
+    def is_i8(self) -> bool: ...
+    def is_i16(self) -> bool: ...
+    def is_f64(self) -> bool: ...
+    def is_complex32(self) -> bool: ...
+
+    def as_i8(self) -> Optional[List[List[int]]]: ...
+    def as_i16(self) -> Optional[List[List[int]]]: ...
+    def as_f64(self) -> Optional[List[List[float]]]: ...
+    def as_complex32(self) -> Optional[List[List[complex]]]: ...
+
+    def to_i8(self) -> List[List[int]]: ...
+    def to_i16(self) -> List[List[int]]: ...
+    def to_f64(self) -> List[List[float]]: ...
+    def to_complex32(self) -> List[List[complex]]: ...
+
+    @staticmethod
+    def from_i8(inner: List[List[int]]) -> "RegisterData": ...
+    @staticmethod
+    def from_i16(inner: List[List[int]]) -> "RegisterData": ...
+    @staticmethod
+    def from_f64(inner: List[List[float]]) -> "RegisterData": ...
+    @staticmethod
+    def from_complex32(inner: List[List[complex]]) -> "RegisterData": ...
diff --git a/crates/python/qcs_sdk/api.pyi b/crates/python/qcs_sdk/api.pyi
new file mode 100644
index 000000000..802806990
--- /dev/null
+++ b/crates/python/qcs_sdk/api.pyi
@@ -0,0 +1,367 @@
+"""
+The qcs_sdk module provides an interface to Rigetti Quantum Cloud Services. Allowing users to compile and run Quil programs on Rigetti quantum processors.
+"""
+from typing import  Dict, List, Optional
+from numbers import Number
+
+from .qpu.client import QcsClient
+
+RecalculationTable = List[str]
+Memory = Dict[str, List[float]]
+PatchValues = Dict[str, List[float]]
+
+class ExecutionError(RuntimeError):
+    """Error encountered during program execution submission or when retrieving results."""
+    ...
+
+
+class TranslationError(RuntimeError):
+    """Error encountered during program translation."""
+    ...
+
+
+class CompilationError(RuntimeError):
+    """Error encountered during program compilation."""
+    ...
+
+
+class RewriteArithmeticError(RuntimeError):
+    """Error encountered rewriting arithmetic for program."""
+    ...
+
+
+class DeviceIsaError(ValueError):
+    """Error while building Instruction Set Architecture."""
+    ...
+
+
+class RewriteArithmeticResults:
+    """
+    The result of a call to [`rewrite_arithmetic`] which provides the information necessary to later patch-in memory values to a compiled program.
+    """
+
+    @property
+    def program(self) -> str:
+        """
+        The resulting program where gate parameter arithmetic has been replaced with memory references. Before execution, the program memory should be updated using the `recalculation_table`.
+        """
+        ...
+    @program.setter
+    def program(self, value: str): ...
+
+    @property
+    def recalculation_table(self) -> List[str]:
+        """ 
+        The recalculation table stores an ordered list of arithmetic expressions, which are to be used when updating the program memory before execution.
+        """
+        ...
+    @recalculation_table.setter
+    def recalculation_table(self, value: List[str]): ...
+
+
+class TranslationResult:
+    """
+    The result of a call to [`translate`] which provides information about the translated program.
+    """
+
+    @property
+    def program(self) -> str:
+        """
+        The compiled program binary.
+        """
+        ...
+    @program.setter
+    def program(self, value: str): ...
+
+    @property
+    def ro_sources(self) -> Optional[dict]:
+        """
+        A mapping from the program's memory references to the key used to index the results map.
+        """
+        ...
+    @ro_sources.setter
+    def ro_sources(self, value: Optional[dict]): ...
+
+
+class ExecutionResult:
+    """Execution readout data from a particular memory location."""
+
+    @property
+    def shape(self) -> List[int]:
+        """The shape of the result data."""
+        ...
+    @shape.setter
+    def shape(self, value: List[int]): ...
+    
+    @property
+    def data(self) -> List[Number | List[float]]:
+        """The result data. Complex numbers are represented as [real, imaginary]."""
+        ...
+    @data.setter
+    def data(self, value: List[Number | List[float]]): ...
+
+    @property
+    def dtype(self) -> str:
+        """The type of the result data (as a `numpy` `dtype`)."""
+        ...
+    @dtype.setter
+    def dtype(self, value: str): ...
+
+
+class ExecutionResults:
+    """Execution readout data for all memory locations."""
+
+    @property
+    def buffers(self) -> Dict[str, ExecutionResult]:
+        """
+        The readout results of execution, mapping a published filter node to its data.
+
+        See `TranslationResult.ro_sources` which provides the mapping from the filter node name to the name of the memory declaration in the source program.
+        """
+        ...
+    @buffers.setter
+    def buffers(self, value: Dict[str, ExecutionResult]): ...
+    
+    @property
+    def execution_duration_microseconds(self) -> Optional[int]:
+        """The time spent executing the program."""
+        ...
+    @execution_duration_microseconds.setter
+    def execution_duration_microseconds(self, value: Optional[int]): ...
+
+
+class Register:
+    """
+    Data from an individual register.
+
+    Variants:
+        - ``i8``: A register of 8-bit integers.
+        - ``i16``: A register of 16-bit integers.
+        - ``i32``: A register of 32-bit integers.
+        - ``f64``: A register of 64-bit floating point numbers.
+        - ``complex64``: A register of 64-bit complex numbers.
+
+    Methods (each per variant):
+        - ``is_*``: if the underlying values are that type.
+        - ``as_*``: if the underlying values are that type, then those values, otherwise ``None``.
+        - ``to_*``: the underlyting values as that type, raises ``ValueError`` if they are not.
+        - ``from_*``: wrap underlying values as this enum type.
+
+    """
+    
+    def is_i8(self) -> bool: ...
+    def is_i16(self) -> bool: ...
+    def is_i32(self) -> bool: ...
+    def is_f64(self) -> bool: ...
+    def is_complex64(self) -> bool: ...
+
+    def as_i8(self) -> Optional[List[int]]: ...
+    def as_i16(self) -> Optional[List[int]]: ...
+    def as_i32(self) -> Optional[List[int]]: ...
+    def as_f64(self) -> Optional[List[float]]: ...
+    def as_complex64(self) -> Optional[List[complex]]: ...
+
+    def to_i8(self) -> List[int]: ...
+    def to_i16(self) -> List[int]: ...
+    def to_i32(self) -> List[int]: ...
+    def to_f64(self) -> List[float]: ...
+    def to_complex64(self) -> List[complex]: ...
+
+    @staticmethod
+    def from_i8(inner: List[int]) -> "Register": ...
+    @staticmethod
+    def from_i16(inner: List[int]) -> "Register": ...
+    @staticmethod
+    def from_i32(inner: List[int]) -> "Register": ...
+    @staticmethod
+    def from_f64(inner: List[float]) -> "Register": ...
+    @staticmethod
+    def from_complex64(inner: List[complex]) -> "Register": ...
+
+
+async def compile(
+    quil: str,
+    target_device: str,
+    client: Optional[QcsClient] = None,
+    *,
+    timeout: int = 30,
+) -> str:
+    """
+    Uses quilc to convert a quil program to native Quil.
+
+    Args:
+        quil: A Quil program.
+        target_device: A JSON encoded description of the Quantum Abstract Machine Architecture.
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Keyword Args:
+        timeout: The number of seconds to wait before timing out. If set to None, there is no timeout (default: 30).
+
+    Returns:
+        An Awaitable that resolves to the native Quil program.
+
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``DeviceIsaError`` If the `target_device` is misconfigured.
+        - ``CompilationError`` If the program could not compile.
+    """
+    ...
+
+
+def rewrite_arithmetic(
+    native_quil: str,
+) -> RewriteArithmeticResults:
+    """
+    Rewrites parametric arithmetic such that all gate parameters are only memory references
+    to a newly declared memory location (__SUBST).
+
+    Args:
+        native_quil: A Quil program.
+
+    Returns:
+        A dictionary with the rewritten program and recalculation table (see `RewriteArithmeticResults`).
+    
+    Raises:
+        - ``TranslationError`` If the program could not be translated.
+        - ``RewriteArithmeticError`` If the program arithmetic cannot be evaluated.
+    """
+    ...
+
+
+def build_patch_values(
+    recalculation_table: RecalculationTable,
+    memory: Memory,
+) -> PatchValues:
+    """
+    Evaluate the expressions in recalculation_table using the numeric values
+    provided in memory.
+
+    Args:
+        recalculation_table: an ordered list of arithmetic expressions, which are to be used when updating the program memory before execution (see `rewrite_arithmetic`).
+        memory: A mapping of symbols to their values (e.g. `{"theta": [0.0]}`).
+
+    Returns:
+        A dictionary that maps each symbol to the value it should be patched with.
+
+    Raises:
+        - ``TranslationError`` If the expressions in `recalculation_table` could not be evaluated.
+    """
+    ...
+
+
+async def translate(
+    native_quil: str,
+    num_shots: int,
+    quantum_processor_id: str,
+    client: Optional[QcsClient] = None,
+) -> TranslationResult:
+    """
+    Translates a native Quil program into an executable.
+
+    Args:
+        native_quil: A Quil program.
+        num_shots: The number of shots to perform.
+        quantum_processor_id: The ID of the quantum processor the executable will run on (e.g. "Aspen-M-2").
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Returns:
+        An Awaitable that resolves to a dictionary with the compiled program, memory descriptors, and readout sources (see `TranslationResult`).
+    
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``TranslationError`` If the `native_quil` program could not be translated.
+    """
+    ...
+
+
+async def submit(
+    program: str,
+    patch_values: Dict[str, List[float]],
+    quantum_processor_id: str,
+    client: Optional[QcsClient] = None,
+) -> str:
+    """
+    Submits an executable `program` to be run on the specified QPU.
+
+    Args:
+        program: An executable program (see `translate`).
+        patch_values: A mapping of symbols to their desired values (see `build_patch_values`).
+        quantum_processor_id: The ID of the quantum processor to run the executable on.
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Returns:
+        An Awaitable that resolves to the ID of the submitted job.
+    
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``ExecutionError`` If there was a problem during program execution.
+    """
+    ...
+
+
+async def retrieve_results(
+    job_id: str,
+    quantum_processor_id: str,
+    client: Optional[QcsClient] = None,
+) -> ExecutionResults:
+    """
+    Fetches results for the corresponding job ID.
+
+    Args:
+        job_id: The ID of the job to retrieve results for.
+        quantum_processor_id: The ID of the quanutum processor the job ran on.
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+
+    Returns:
+        An Awaitable that resolves to a dictionary describing the results of the execution and its duration (see `ExecutionResults`).
+
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``ExecutionError`` If there was a problem fetching execution results.
+    """
+    ...
+
+
+async def get_quilc_version(
+    client: Optional[QcsClient] = None,
+) -> str:
+    """
+    Returns the version number of the running quilc server.
+
+    Args:
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+    
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``CompilationError`` If there is an issue fetching the version from the quilc compiler.
+    """
+    ...
+
+
+async def get_quilc_version(
+    client: Optional[QcsClient] = None,
+) -> str:
+    """
+    Returns the version number of the running quilc server.
+
+    Args:
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+    
+    Raises:
+        - ``LoadError`` If there is an issue loading the QCS Client configuration.
+        - ``CompilationError`` If there is an issue fetching the version from the quilc compiler.
+    """
+    ...
+
+async def list_quantum_processors(
+    client: Optional[QcsClient] = None,
+    timeout: Optional[float] = None,
+) -> List[str]:
+    """
+    Returns all available Quantum Processor IDs.
+
+    Args:
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        timeout: Maximum duration to wait for API calls to complete, in seconds.
+    """
+    ...
\ No newline at end of file
diff --git a/crates/python/py.typed b/crates/python/qcs_sdk/py.typed
similarity index 100%
rename from crates/python/py.typed
rename to crates/python/qcs_sdk/py.typed
diff --git a/crates/python/qcs_sdk/qpu/__init__.pyi b/crates/python/qcs_sdk/qpu/__init__.pyi
new file mode 100644
index 000000000..e596a2f0a
--- /dev/null
+++ b/crates/python/qcs_sdk/qpu/__init__.pyi
@@ -0,0 +1,2 @@
+class QcsIsaError(RuntimeError):
+    ...
diff --git a/crates/python/qcs_sdk/qpu/client.pyi b/crates/python/qcs_sdk/qpu/client.pyi
new file mode 100644
index 000000000..c59311ed5
--- /dev/null
+++ b/crates/python/qcs_sdk/qpu/client.pyi
@@ -0,0 +1,79 @@
+from typing import Optional
+
+
+class QcsClient:
+    """
+    Configuration for connecting and authenticating to QCS API resources.
+    """
+
+    def __new__(
+        cls,
+        tokens: Optional[QcsClientTokens] = None,
+        api_url: Optional[str] = None,
+        auth_server: Optional[QcsClientAuthServer] = None,
+        grpc_api_url: Optional[str] = None,
+        quilc_url: Optional[str] = None,
+        qvm_url: Optional[str] = None,
+    ) -> "QcsClient":
+        """
+        Construct a client from scratch.
+        
+        Use ``QcsClient.load`` to construct an environment-based profile.
+        """
+        ...
+    
+    @staticmethod
+    def load(
+        profile_name: Optional[str] = None,
+        use_gateway: Optional[bool] = None,
+    ) -> "QcsClient":
+        """
+        Load a QcsClient configuration using an environment-based configuration.
+
+        See for details: https://docs.rigetti.com/qcs/references/qcs-client-configuration#environment-variables-and-configuration-files
+        """
+        ...
+
+
+class QcsClientAuthServer:
+    """Authentication server configuration for the QCS API."""
+
+    @property
+    def client_id(self) -> str: ...
+    @client_id.setter
+    def client_id(self, value: str): ...
+
+    @property
+    def issuer(self) -> str: ...
+    @issuer.setter
+    def issuer(self, value: str): ...
+
+
+class QcsClientTokens:
+    """Authentication tokens for the QCS API."""
+
+    @property
+    def bearer_access_token(self) -> Optional[str]: ...
+    @bearer_access_token.setter
+    def bearer_access_token(self, value: Optional[str]): ...
+
+    @property
+    def refresh_token(self) -> Optional[str]: ...
+    @refresh_token.setter
+    def refresh_token(self, value: Optional[str]): ...
+
+
+class QcsGrpcClientError(RuntimeError):
+    """Error encountered while loading a QCS gRPC API client."""
+
+
+class QcsGrpcEndpointError(RuntimeError):
+    """Error when trying to resolve the QCS gRPC API endpoint."""
+
+
+class QcsGrpcError(RuntimeError):
+    """Error during QCS gRPC API requests."""
+
+
+class QcsLoadError(RuntimeError):
+    """Error encountered while loading the QCS API client configuration."""
diff --git a/crates/python/qcs_sdk/qpu/quilc.pyi b/crates/python/qcs_sdk/qpu/quilc.pyi
new file mode 100644
index 000000000..8d36e73b5
--- /dev/null
+++ b/crates/python/qcs_sdk/qpu/quilc.pyi
@@ -0,0 +1,29 @@
+from typing import Any, Dict, Optional
+
+DEFAULT_COMPILER_TIMEOUT: int
+
+
+class QuilcError(RuntimeError):
+    ...
+
+
+class CompilerOpts:
+    """A set of options that determine the behavior of compiling programs with quilc."""
+
+    @property
+    def timeout(self) -> Optional[int]:
+        """The number of seconds to wait before timing out. If `None`, there is no timeout."""
+        ...
+
+    def __new__(
+        cls,
+        timeout: Optional[int] = DEFAULT_COMPILER_TIMEOUT
+    ) -> "CompilerOpts":
+        ...
+
+    @staticmethod
+    def default() -> "CompilerOpts": ...
+
+
+class TargetDevice:
+    ...
diff --git a/crates/python/src/api.rs b/crates/python/src/api.rs
new file mode 100644
index 000000000..6c985baad
--- /dev/null
+++ b/crates/python/src/api.rs
@@ -0,0 +1,248 @@
+use std::{collections::HashMap, time::Duration};
+
+use pyo3::{
+    create_exception,
+    exceptions::{PyRuntimeError, PyValueError},
+    prelude::*,
+    pyfunction,
+    types::{PyComplex, PyDict, PyFloat, PyInt, PyList, PyString},
+    Py, PyResult,
+};
+use qcs::{
+    api::{
+        list_quantum_processors, ExecutionResult, ExecutionResults, Register,
+        RewriteArithmeticResult, TranslationResult,
+    },
+    qpu::quilc::{CompilerOpts, TargetDevice, DEFAULT_COMPILER_TIMEOUT},
+};
+use rigetti_pyo3::{
+    create_init_submodule, py_wrap_data_struct, py_wrap_error, py_wrap_type, py_wrap_union_enum,
+    wrap_error, ToPython, ToPythonError,
+};
+
+use crate::qpu::client::PyQcsClient;
+
+create_init_submodule! {
+    classes: [
+        PyExecutionResult,
+        PyExecutionResults,
+        PyRegister,
+        PyRewriteArithmeticResult,
+        PyTranslationResult
+    ],
+    errors: [
+        ExecutionError,
+        TranslationError,
+        CompilationError,
+        RewriteArithmeticError,
+        DeviceIsaError,
+        QcsListQuantumProcessorsError,
+        QcsSubmitError
+    ],
+    funcs: [
+        compile,
+        rewrite_arithmetic,
+        translate,
+        submit,
+        retrieve_results,
+        build_patch_values,
+        get_quilc_version,
+        py_list_quantum_processors
+    ],
+}
+
+py_wrap_data_struct! {
+    PyRewriteArithmeticResult(RewriteArithmeticResult) as "RewriteArithmeticResult" {
+        program: String => Py<PyString>,
+        recalculation_table: Vec<String> => Py<PyList>
+    }
+}
+
+py_wrap_data_struct! {
+    PyTranslationResult(TranslationResult) as "TranslationResult" {
+        program: String => Py<PyString>,
+        ro_sources: Option<HashMap<String, String>> => Option<Py<PyDict>>
+    }
+}
+
+py_wrap_type! {
+    PyExecutionResults(ExecutionResults) as "ExecutionResults";
+}
+
+py_wrap_union_enum! {
+    PyRegister(Register) as "Register" {
+        f64: F64 => Vec<Py<PyFloat>>,
+        i16: I16 => Vec<Py<PyInt>>,
+        i32: I32 => Vec<Py<PyInt>>,
+        i8: I8 => Vec<Py<PyInt>>,
+        complex64: Complex64 => Vec<Py<PyComplex>>
+    }
+}
+
+py_wrap_type! {
+    PyExecutionResult(ExecutionResult) as "ExecutionResult";
+}
+
+create_exception!(qcs, ExecutionError, PyRuntimeError);
+create_exception!(qcs, TranslationError, PyRuntimeError);
+create_exception!(qcs, CompilationError, PyRuntimeError);
+create_exception!(qcs, RewriteArithmeticError, PyRuntimeError);
+create_exception!(qcs, DeviceIsaError, PyValueError);
+
+wrap_error!(SubmitError(qcs::api::SubmitError));
+py_wrap_error!(api, SubmitError, QcsSubmitError, PyRuntimeError);
+
+wrap_error!(ListQuantumProcessorsError(
+    qcs::api::ListQuantumProcessorsError
+));
+py_wrap_error!(
+    api,
+    ListQuantumProcessorsError,
+    QcsListQuantumProcessorsError,
+    PyRuntimeError
+);
+
+/// Get the keyword `key` value from `kwds` if it is of type `Option<T>`, else `None`.
+/// Note that values present at `key` that cannot be extracted into `Option<T>` are discarded.
+fn get_kwd<'a, T: FromPyObject<'a>>(kwds: Option<&'a PyDict>, key: &str) -> Option<T> {
+    if let Some(kwds) = kwds {
+        kwds.get_item(key)
+            .map(|value| value.extract().ok())
+            .unwrap_or_default()
+    } else {
+        None
+    }
+}
+
+#[pyfunction(client = "None", kwds = "**")]
+pub fn compile<'a>(
+    py: Python<'a>,
+    quil: String,
+    target_device: String,
+    client: Option<PyQcsClient>,
+    kwds: Option<&PyDict>,
+) -> PyResult<&'a PyAny> {
+    let target_device: TargetDevice =
+        serde_json::from_str(&target_device).map_err(|e| DeviceIsaError::new_err(e.to_string()))?;
+
+    let compiler_timeout = get_kwd(kwds, "timeout").or(Some(DEFAULT_COMPILER_TIMEOUT));
+    let protoquil: Option<bool> = get_kwd(kwds, "protoquil");
+
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let client = PyQcsClient::get_or_create_client(client).await?;
+        let options = CompilerOpts::default()
+            .with_timeout(compiler_timeout)
+            .with_protoquil(protoquil);
+        let result = qcs::api::compile(&quil, target_device, &client, options)
+            .map_err(|e| CompilationError::new_err(e.to_string()))?;
+        Ok(result)
+    })
+}
+
+#[pyfunction]
+pub fn rewrite_arithmetic(native_quil: String) -> PyResult<PyRewriteArithmeticResult> {
+    let native_program = native_quil
+        .parse::<quil_rs::Program>()
+        .map_err(|e| TranslationError::new_err(e.to_string()))?;
+    let result = qcs::api::rewrite_arithmetic(native_program)
+        .map_err(|e| RewriteArithmeticError::new_err(e.to_string()))?;
+    let pyed = result.into();
+    Ok(pyed)
+}
+
+#[pyfunction]
+pub fn build_patch_values(
+    py: Python<'_>,
+    recalculation_table: Vec<String>,
+    memory: HashMap<String, Vec<f64>>,
+) -> PyResult<Py<PyDict>> {
+    let memory = memory
+        .into_iter()
+        .map(|(k, v)| (k.into_boxed_str(), v))
+        .collect();
+    let patch_values = qcs::api::build_patch_values(&recalculation_table, &memory)
+        .map_err(TranslationError::new_err)?;
+    patch_values
+        .into_iter()
+        .map(|(k, v)| (k.to_string(), v))
+        .collect::<HashMap<_, _>>()
+        .to_python(py)
+}
+
+#[pyfunction(client = "None")]
+pub fn translate(
+    py: Python<'_>,
+    native_quil: String,
+    num_shots: u16,
+    quantum_processor_id: String,
+    client: Option<PyQcsClient>,
+) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let client = PyQcsClient::get_or_create_client(client).await?;
+        let result = qcs::api::translate(&native_quil, num_shots, &quantum_processor_id, &client)
+            .await
+            .map_err(|e| TranslationError::new_err(e.to_string()))?;
+        Python::with_gil(|py| PyTranslationResult::from(result).to_python(py))
+    })
+}
+
+#[pyfunction(client = "None")]
+pub fn submit(
+    py: Python<'_>,
+    program: String,
+    patch_values: HashMap<String, Vec<f64>>,
+    quantum_processor_id: String,
+    client: Option<PyQcsClient>,
+) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let client = PyQcsClient::get_or_create_client(client).await?;
+        let job_id = qcs::api::submit(&program, patch_values, &quantum_processor_id, &client)
+            .await
+            .map_err(|e| ExecutionError::new_err(e.to_string()))?;
+        Ok(Python::with_gil(|_py| job_id))
+    })
+}
+
+#[pyfunction(client = "None")]
+pub fn retrieve_results(
+    py: Python<'_>,
+    job_id: String,
+    quantum_processor_id: String,
+    client: Option<PyQcsClient>,
+) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let client = PyQcsClient::get_or_create_client(client).await?;
+        let results = qcs::api::retrieve_results(&job_id, &quantum_processor_id, &client)
+            .await
+            .map_err(|e| ExecutionError::new_err(e.to_string()))?;
+        Ok(PyExecutionResults::from(results))
+    })
+}
+
+#[pyfunction(client = "None")]
+pub fn get_quilc_version(py: Python<'_>, client: Option<PyQcsClient>) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let client = PyQcsClient::get_or_create_client(client).await?;
+        let version = qcs::api::get_quilc_version(&client)
+            .map_err(|e| CompilationError::new_err(e.to_string()))?;
+        Ok(version)
+    })
+}
+
+#[pyfunction(client = "None", timeout = "None")]
+#[pyo3(name = "list_quantum_processors")]
+pub fn py_list_quantum_processors(
+    py: Python<'_>,
+    client: Option<PyQcsClient>,
+    timeout: Option<f64>,
+) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let client = PyQcsClient::get_or_create_client(client).await?;
+        let timeout = timeout.map(Duration::from_secs_f64);
+        let names = list_quantum_processors(&client, timeout)
+            .await
+            .map_err(ListQuantumProcessorsError::from)
+            .map_err(ListQuantumProcessorsError::to_py_err)?;
+        Ok(names)
+    })
+}
diff --git a/crates/python/src/executable.rs b/crates/python/src/executable.rs
new file mode 100644
index 000000000..be5f43c3d
--- /dev/null
+++ b/crates/python/src/executable.rs
@@ -0,0 +1,171 @@
+use std::sync::Arc;
+
+use pyo3::{pyclass, FromPyObject};
+use qcs::{Error, Executable, JobHandle, Qpu, Qvm, Service};
+use rigetti_pyo3::{
+    impl_as_mut_for_wrapper, py_wrap_error, py_wrap_simple_enum, py_wrap_type,
+    pyo3::{exceptions::PyRuntimeError, pymethods, types::PyDict, Py, PyAny, PyResult, Python},
+    wrap_error, PyWrapper, ToPython, ToPythonError,
+};
+use tokio::sync::Mutex;
+
+use crate::qpu::quilc::PyCompilerOpts;
+
+wrap_error!(ExecutionError(Error));
+
+py_wrap_error!(
+    executable,
+    ExecutionError,
+    QcsExecutionError,
+    PyRuntimeError
+);
+
+// Because Python is garbage-collected, no lifetimes can be guaranteed except `'static`.
+//
+// `Arc<Mutex<>>` to work around https://github.com/awestlake87/pyo3-asyncio/issues/50
+py_wrap_type! {
+    PyExecutable(Arc<Mutex<Executable<'static, 'static>>>) as "Executable";
+}
+
+impl_as_mut_for_wrapper!(PyExecutable);
+
+#[pyclass]
+#[pyo3(name = "ExeParameter")]
+#[derive(FromPyObject)]
+pub struct PyParameter {
+    #[pyo3(get, set)]
+    pub name: String,
+    #[pyo3(get, set)]
+    pub index: usize,
+    #[pyo3(get, set)]
+    pub value: f64,
+}
+
+#[pymethods]
+impl PyParameter {
+    #[new]
+    pub fn new(name: String, index: usize, value: f64) -> Self {
+        Self { name, index, value }
+    }
+}
+
+#[pymethods]
+impl PyExecutable {
+    #[new]
+    #[args(
+        "/",
+        registers = "Vec::new()",
+        parameters = "Vec::new()",
+        shots = "None",
+        compile_with_quilc = "None",
+        compiler_options = "None"
+    )]
+    pub fn new(
+        quil: String,
+        registers: Vec<String>,
+        parameters: Vec<PyParameter>,
+        shots: Option<u16>,
+        compile_with_quilc: Option<bool>,
+        compiler_options: Option<PyCompilerOpts>,
+    ) -> Self {
+        let mut exe = Executable::from_quil(quil);
+
+        for reg in registers {
+            exe = exe.read_from(reg);
+        }
+
+        for param in parameters {
+            exe.with_parameter(param.name, param.index, param.value);
+        }
+
+        if let Some(shots) = shots {
+            exe = exe.with_shots(shots);
+        }
+
+        if let Some(compile_with_quilc) = compile_with_quilc {
+            exe = exe.compile_with_quilc(compile_with_quilc);
+        }
+
+        if let Some(options) = compiler_options {
+            exe = exe.compiler_options(options.into_inner());
+        }
+
+        Self::from(Arc::new(Mutex::new(exe)))
+    }
+
+    pub fn execute_on_qvm<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> {
+        let arc = self.as_inner().clone();
+        pyo3_asyncio::tokio::local_future_into_py(py, async move {
+            arc.lock()
+                .await
+                .execute_on_qvm()
+                .await
+                .map(Qvm::from)
+                .map(|qvm| Python::with_gil(|py| qvm.to_python(py)))
+                .map_err(ExecutionError::from)
+                .map_err(ExecutionError::to_py_err)?
+        })
+    }
+
+    pub fn execute_on_qpu<'py>(
+        &self,
+        py: Python<'py>,
+        quantum_processor_id: String,
+    ) -> PyResult<&'py PyAny> {
+        let arc = self.as_inner().clone();
+        pyo3_asyncio::tokio::local_future_into_py(py, async move {
+            arc.lock()
+                .await
+                .execute_on_qpu(quantum_processor_id)
+                .await
+                .map(Qpu::from)
+                .map(|qpu| Python::with_gil(|py| qpu.to_python(py)))
+                .map_err(ExecutionError::from)
+                .map_err(ExecutionError::to_py_err)?
+        })
+    }
+
+    pub fn retrieve_results<'py>(
+        &mut self,
+        py: Python<'py>,
+        job_handle: PyJobHandle,
+    ) -> PyResult<&'py PyAny> {
+        let arc = self.as_inner().clone();
+        pyo3_asyncio::tokio::local_future_into_py(py, async move {
+            arc.lock()
+                .await
+                .retrieve_results(job_handle.into_inner())
+                .await
+                .map(Qpu::from)
+                .map(|qpu| Python::with_gil(|py| qpu.to_python(py)))
+                .map_err(ExecutionError::from)
+                .map_err(ExecutionError::to_py_err)?
+        })
+    }
+}
+
+py_wrap_simple_enum! {
+    PyService(Service) as "Service" {
+        Quilc,
+        Qvm,
+        Qcs,
+        Qpu
+    }
+}
+
+py_wrap_type! {
+    PyJobHandle(JobHandle<'static>);
+}
+
+#[pymethods]
+impl PyJobHandle {
+    #[getter]
+    pub fn job_id(&self) -> &str {
+        self.as_inner().job_id()
+    }
+
+    #[getter]
+    pub fn readout_map(&self, py: Python) -> PyResult<Py<PyDict>> {
+        self.as_inner().readout_map().to_python(py)
+    }
+}
diff --git a/crates/python/src/execution_data.rs b/crates/python/src/execution_data.rs
new file mode 100644
index 000000000..076881545
--- /dev/null
+++ b/crates/python/src/execution_data.rs
@@ -0,0 +1,61 @@
+use std::{collections::HashMap, time::Duration};
+
+use pyo3::{
+    pymethods,
+    types::{PyDelta, PyDict},
+    Py, PyResult, Python,
+};
+use qcs::{Qpu, Qvm, ReadoutMap, RegisterData};
+use qcs_api_client_grpc::models::controller::{readout_values::Values, ReadoutValues};
+use rigetti_pyo3::{py_wrap_data_struct, py_wrap_type, PyWrapper, ToPython};
+
+use crate::grpc::models::controller::PyReadoutValuesValues;
+use crate::register_data::PyRegisterData;
+
+py_wrap_data_struct! {
+    PyQvm(Qvm) as "QVM" {
+        registers: HashMap<String, RegisterData> => HashMap<String, PyRegisterData> => Py<PyDict>,
+        duration: Option<Duration> => Option<Py<PyDelta>>
+    }
+}
+
+py_wrap_data_struct! {
+    PyQpu(Qpu) as "QPU" {
+        readout_data: ReadoutMap => PyReadoutMap,
+        duration: Option<Duration> => Option<Py<PyDelta>>
+    }
+}
+
+// From gRPC
+py_wrap_data_struct! {
+    PyReadoutValues(ReadoutValues) as "ReadoutValues" {
+        values: Option<Values> => Option<PyReadoutValuesValues>
+    }
+}
+
+py_wrap_type! {
+    PyReadoutMap(ReadoutMap) as "ReadoutMap";
+}
+
+#[pymethods]
+impl PyReadoutMap {
+    pub fn get_readout_values(&self, field: String, index: u64) -> Option<PyReadoutValues> {
+        self.as_inner()
+            .get_readout_values(field, index)
+            .map(PyReadoutValues::from)
+    }
+
+    pub fn get_readout_values_for_field(
+        &self,
+        py: Python,
+        field: &str,
+    ) -> PyResult<Option<Vec<Option<PyReadoutValues>>>> {
+        let op = self.as_inner().get_readout_values_for_field(field)?;
+        op.map(|list| {
+            list.into_iter()
+                .map(|op| op.to_python(py))
+                .collect::<PyResult<_>>()
+        })
+        .transpose()
+    }
+}
diff --git a/crates/python/src/grpc/mod.rs b/crates/python/src/grpc/mod.rs
new file mode 100644
index 000000000..c446ac883
--- /dev/null
+++ b/crates/python/src/grpc/mod.rs
@@ -0,0 +1,1 @@
+pub mod models;
diff --git a/crates/python/src/grpc/models/controller.rs b/crates/python/src/grpc/models/controller.rs
new file mode 100644
index 000000000..cd8302023
--- /dev/null
+++ b/crates/python/src/grpc/models/controller.rs
@@ -0,0 +1,59 @@
+use pyo3::{
+    prelude::*,
+    types::{PyComplex, PyInt, PyList},
+};
+use qcs_api_client_grpc::models::controller::{
+    readout_values::Values, Complex64, Complex64ReadoutValues, IntegerReadoutValues, ReadoutValues,
+};
+use rigetti_pyo3::{num_complex::Complex32 as NumComplex32, py_wrap_struct};
+use rigetti_pyo3::{py_wrap_data_struct, py_wrap_union_enum, PyTryFrom, ToPython};
+
+py_wrap_data_struct! {
+    PyReadoutValues(ReadoutValues) as "ReadoutValues" {
+        values: Option<Values> => Option<PyReadoutValuesValues>
+    }
+}
+
+py_wrap_union_enum! {
+    PyReadoutValuesValues(Values) as "ReadoutValuesValues" {
+        integer_values: IntegerValues => PyIntegerReadoutValues,
+        complex_values: ComplexValues => PyComplexReadoutValues
+    }
+}
+
+py_wrap_data_struct! {
+    PyIntegerReadoutValues(IntegerReadoutValues) as "IntegerReadoutValues" {
+        values: Vec<i32> => Vec<Py<PyInt>>
+    }
+}
+
+py_wrap_struct! {
+    PyComplexReadoutValues(Complex64ReadoutValues) as "ComplexReadoutValues" {
+        py -> rs {
+            list: Py<PyList> => Complex64ReadoutValues {
+                let list = <Vec<Py<PyComplex>>>::py_try_from(py, &list)?;
+                let values = list.into_iter().map(|complex| {
+                    let complex = NumComplex32::py_try_from(py, &complex)?;
+                    Ok::<_, PyErr>(Complex64 {
+                        real: Some(complex.re),
+                        imaginary: Some(complex.im),
+                    })
+                }).collect::<Result<_, _>>()?;
+
+                Ok::<_, PyErr>(Complex64ReadoutValues { values })
+            }
+        },
+        rs -> py {
+            values: Complex64ReadoutValues => Py<PyList> {
+                let list = values.values.into_iter().map(|complex| {
+                    NumComplex32 {
+                        re: complex.real.unwrap_or_default(),
+                        im: complex.imaginary.unwrap_or_default(),
+                    }
+                }).collect::<Vec<_>>();
+
+                list.to_python(py)
+            }
+        }
+    }
+}
diff --git a/crates/python/src/grpc/models/mod.rs b/crates/python/src/grpc/models/mod.rs
new file mode 100644
index 000000000..cb9e0ac5c
--- /dev/null
+++ b/crates/python/src/grpc/models/mod.rs
@@ -0,0 +1,1 @@
+pub mod controller;
diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs
index 5ef3b54b4..6debef875 100644
--- a/crates/python/src/lib.rs
+++ b/crates/python/src/lib.rs
@@ -1,139 +1,47 @@
-use pythonize::pythonize;
-use qcs::api;
-use qcs::qpu::client::Qcs;
-use qcs::qpu::quilc::TargetDevice;
-use std::collections::HashMap;
-
-use pyo3::{
-    create_exception,
-    exceptions::{PyRuntimeError, PyValueError},
-    prelude::*,
-};
-
-create_exception!(qcs, InvalidConfigError, PyRuntimeError);
-create_exception!(qcs, ExecutionError, PyRuntimeError);
-create_exception!(qcs, TranslationError, PyRuntimeError);
-create_exception!(qcs, CompilationError, PyRuntimeError);
-create_exception!(qcs, RewriteArithmeticError, PyRuntimeError);
-create_exception!(qcs, DeviceIsaError, PyValueError);
-
-#[pyfunction]
-fn compile(
-    py: Python<'_>,
-    quil: String,
-    target_device: String,
-    protoquil: Option<bool>,
-) -> PyResult<&PyAny> {
-    let target_device: TargetDevice =
-        serde_json::from_str(&target_device).map_err(|e| DeviceIsaError::new_err(e.to_string()))?;
-    pyo3_asyncio::tokio::future_into_py(py, async move {
-        let client = Qcs::load()
-            .await
-            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
-        let result = api::compile(&quil, target_device, protoquil, &client)
-            .map_err(|e| CompilationError::new_err(e.to_string()))?;
-        Ok(Python::with_gil(|_py| result))
-    })
-}
-
-#[pyfunction]
-fn rewrite_arithmetic(py: Python<'_>, native_quil: String) -> PyResult<PyObject> {
-    let native_program = native_quil
-        .parse::<quil_rs::Program>()
-        .map_err(|e| TranslationError::new_err(e.to_string()))?;
-    let result = api::rewrite_arithmetic(native_program)
-        .map_err(|e| RewriteArithmeticError::new_err(e.to_string()))?;
-    let pyed = pythonize(py, &result).map_err(|e| TranslationError::new_err(e.to_string()))?;
-    Ok(pyed)
-}
-
-#[pyfunction]
-fn build_patch_values(
-    py: Python<'_>,
-    recalculation_table: Vec<String>,
-    memory: HashMap<String, Vec<f64>>,
-) -> PyResult<PyObject> {
-    let memory = memory
-        .into_iter()
-        .map(|(k, v)| (k.into_boxed_str(), v))
-        .collect();
-    let patch_values = api::build_patch_values(&recalculation_table, &memory)
-        .map_err(TranslationError::new_err)?;
-    let patch_values =
-        pythonize(py, &patch_values).map_err(|e| TranslationError::new_err(e.to_string()))?;
-    Ok(patch_values)
-}
-
-#[pyfunction]
-fn translate(
-    py: Python<'_>,
-    native_quil: String,
-    num_shots: u16,
-    quantum_processor_id: String,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
-        let client = Qcs::load()
-            .await
-            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
-        let result = api::translate(&native_quil, num_shots, &quantum_processor_id, &client)
-            .await
-            .map_err(|e| TranslationError::new_err(e.to_string()))?;
-        let result = Python::with_gil(|py| {
-            pythonize(py, &result).map_err(|e| TranslationError::new_err(e.to_string()))
-        })?;
-        Ok(result)
-    })
-}
-
-#[pyfunction]
-fn submit(
-    py: Python<'_>,
-    program: String,
-    patch_values: HashMap<String, Vec<f64>>,
-    quantum_processor_id: String,
-    use_gateway: bool,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
-        let client = Qcs::load()
-            .await
-            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?
-            .with_use_gateway(use_gateway);
-        let job_id = api::submit(&program, patch_values, &quantum_processor_id, &client)
-            .await
-            .map_err(|e| ExecutionError::new_err(e.to_string()))?;
-        Ok(Python::with_gil(|_py| job_id))
-    })
-}
-
-#[pyfunction]
-fn retrieve_results(
-    py: Python<'_>,
-    job_id: String,
-    quantum_processor_id: String,
-    use_gateway: bool,
-) -> PyResult<&PyAny> {
-    pyo3_asyncio::tokio::future_into_py(py, async move {
-        let client = Qcs::load()
-            .await
-            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?
-            .with_use_gateway(use_gateway);
-        let results = api::retrieve_results(&job_id, &quantum_processor_id, &client)
-            .await
-            .map_err(|e| ExecutionError::new_err(e.to_string()))?;
-        let results = Python::with_gil(|py| {
-            pythonize(py, &results).map_err(|e| ExecutionError::new_err(e.to_string()))
-        })?;
-        Ok(results)
-    })
+use pyo3::prelude::*;
+use rigetti_pyo3::create_init_submodule;
+
+use executable::QcsExecutionError;
+
+pub mod api;
+pub mod executable;
+pub mod execution_data;
+pub mod grpc;
+pub mod qpu;
+pub mod register_data;
+
+create_init_submodule! {
+    classes: [
+        execution_data::PyQpu,
+        execution_data::PyQvm,
+        execution_data::PyReadoutMap,
+        executable::PyExecutable,
+        executable::PyParameter,
+        executable::PyJobHandle,
+        executable::PyService,
+        register_data::PyRegisterData,
+        qpu::client::PyQcsClient
+    ],
+    errors: [
+        QcsExecutionError
+    ],
+    funcs: [
+        api::compile,
+        api::rewrite_arithmetic,
+        api::translate,
+        api::submit,
+        api::retrieve_results,
+        api::build_patch_values,
+        api::get_quilc_version,
+        api::py_list_quantum_processors
+    ],
+    submodules: [
+        "api": api::init_submodule,
+        "qpu": qpu::init_submodule
+    ],
 }
 
 #[pymodule]
-fn qcs_sdk(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
-    m.add_function(wrap_pyfunction!(compile, m)?)?;
-    m.add_function(wrap_pyfunction!(rewrite_arithmetic, m)?)?;
-    m.add_function(wrap_pyfunction!(translate, m)?)?;
-    m.add_function(wrap_pyfunction!(submit, m)?)?;
-    m.add_function(wrap_pyfunction!(retrieve_results, m)?)?;
-    m.add_function(wrap_pyfunction!(build_patch_values, m)?)?;
-    Ok(())
+fn qcs_sdk(py: Python<'_>, m: &PyModule) -> PyResult<()> {
+    init_submodule("qcs_sdk", py, m)
 }
diff --git a/crates/python/src/qpu/client.rs b/crates/python/src/qpu/client.rs
new file mode 100644
index 000000000..87b3a2f42
--- /dev/null
+++ b/crates/python/src/qpu/client.rs
@@ -0,0 +1,220 @@
+use pyo3_asyncio::tokio::future_into_py;
+use qcs_api_client_common::{
+    configuration::{AuthServer, BuildError, ClientConfigurationBuilder, Tokens},
+    ClientConfiguration,
+};
+use rigetti_pyo3::{
+    create_init_submodule, py_wrap_data_struct, py_wrap_error, py_wrap_type,
+    pyo3::{
+        conversion::IntoPy, exceptions::PyRuntimeError, pyclass, pyclass::CompareOp, pymethods,
+        types::PyString, FromPyObject, Py, PyAny, PyObject, PyResult, Python,
+    },
+    wrap_error, ToPythonError,
+};
+
+use qcs::qpu::Qcs;
+
+create_init_submodule! {
+    classes: [
+        PyQcsClient,
+        PyQcsClientAuthServer,
+        PyQcsClientTokens
+    ],
+    errors: [
+        QcsGrpcClientError,
+        QcsGrpcEndpointError,
+        QcsGrpcError,
+        QcsLoadError
+    ],
+}
+
+wrap_error! {
+    LoadError(qcs::qpu::client::LoadError);
+}
+py_wrap_error!(client, LoadError, QcsLoadError, PyRuntimeError);
+
+wrap_error! {
+    GrpcError(qcs::qpu::client::GrpcError);
+}
+py_wrap_error!(client, GrpcError, QcsGrpcError, PyRuntimeError);
+
+wrap_error! {
+    GrpcClientError(qcs::qpu::client::GrpcClientError);
+}
+py_wrap_error!(client, GrpcClientError, QcsGrpcClientError, PyRuntimeError);
+
+wrap_error! {
+    GrpcEndpointError(qcs::qpu::client::GrpcEndpointError);
+}
+py_wrap_error!(
+    client,
+    GrpcEndpointError,
+    QcsGrpcEndpointError,
+    PyRuntimeError
+);
+
+wrap_error!(ConfigurationBuildError(BuildError));
+py_wrap_error!(
+    qcs,
+    ConfigurationBuildError,
+    QcsConfigurationBuildError,
+    PyRuntimeError
+);
+
+/// The fields on qcs_api_client_common::client::AuthServer are not public.
+#[pyclass]
+#[pyo3(name = "QcsClientAuthServer")]
+#[derive(FromPyObject)]
+pub struct PyQcsClientAuthServer {
+    #[pyo3(get, set)]
+    pub client_id: Option<String>,
+    #[pyo3(get, set)]
+    pub issuer: Option<String>,
+}
+
+impl From<PyQcsClientAuthServer> for AuthServer {
+    fn from(value: PyQcsClientAuthServer) -> Self {
+        let mut auth_server = AuthServer::default();
+        if let Some(client_id) = value.client_id {
+            auth_server = auth_server.set_client_id(client_id);
+        }
+        if let Some(issuer) = value.issuer {
+            auth_server = auth_server.set_issuer(issuer);
+        }
+        auth_server
+    }
+}
+
+#[pymethods]
+impl PyQcsClientAuthServer {
+    #[new]
+    #[args(client_id = "None", issuer = "None")]
+    pub fn new(client_id: Option<String>, issuer: Option<String>) -> Self {
+        Self { client_id, issuer }
+    }
+}
+
+py_wrap_data_struct! {
+    PyQcsClientTokens(Tokens) as "QcsClientTokens" {
+        bearer_access_token: Option<String> => Option<Py<PyString>>,
+        refresh_token: Option<String> => Option<Py<PyString>>
+    }
+}
+
+#[pymethods]
+impl PyQcsClientTokens {
+    #[new]
+    #[args(bearer_access_token = "None", refresh_token = "None")]
+    pub fn new(bearer_access_token: Option<String>, refresh_token: Option<String>) -> Self {
+        Self(Tokens {
+            bearer_access_token,
+            refresh_token,
+        })
+    }
+}
+
+py_wrap_type! {
+    PyQcsClient(Qcs) as "QcsClient";
+}
+
+impl PyQcsClient {
+    pub(crate) async fn get_or_create_client(client: Option<Self>) -> PyResult<Qcs> {
+        Ok(match client {
+            Some(client) => client.into(),
+            None => Qcs::load()
+                .await
+                .map_err(LoadError::from)
+                .map_err(LoadError::to_py_err)?,
+        })
+    }
+}
+
+impl PartialEq for PyQcsClient {
+    fn eq(&self, other: &Self) -> bool {
+        format!("{:?}", self.0) == format!("{:?}", other.0)
+    }
+}
+
+#[pymethods]
+impl PyQcsClient {
+    #[new]
+    #[args(
+        "/",
+        tokens = "None",
+        api_url = "None",
+        auth_server = "None",
+        grpc_api_url = "None",
+        quilc_url = "None",
+        qvm_url = "None"
+    )]
+    pub fn new(
+        tokens: Option<PyQcsClientTokens>,
+        api_url: Option<String>,
+        auth_server: Option<PyQcsClientAuthServer>,
+        grpc_api_url: Option<String>,
+        quilc_url: Option<String>,
+        qvm_url: Option<String>,
+    ) -> PyResult<Self> {
+        let mut builder = ClientConfigurationBuilder::default();
+        if let Some(tokens) = tokens {
+            builder = builder.set_tokens(tokens.into());
+        }
+        if let Some(api_url) = api_url {
+            builder = builder.set_api_url(api_url);
+        }
+        if let Some(auth_server) = auth_server {
+            builder = builder.set_auth_server(auth_server.into());
+        }
+        if let Some(grpc_api_url) = grpc_api_url {
+            builder = builder.set_grpc_api_url(grpc_api_url);
+        }
+        if let Some(quilc_url) = quilc_url {
+            builder = builder.set_quilc_url(quilc_url);
+        }
+        if let Some(qvm_url) = qvm_url {
+            builder = builder.set_grpc_api_url(qvm_url);
+        }
+        let client = builder
+            .build()
+            .map(Qcs::with_config)
+            .map_err(ConfigurationBuildError::from)
+            .map_err(ConfigurationBuildError::to_py_err)?;
+
+        Ok(Self(client))
+    }
+
+    #[staticmethod]
+    #[args("/", profile_name = "None", use_gateway = "None")]
+    pub fn load(
+        py: Python<'_>,
+        profile_name: Option<String>,
+        use_gateway: Option<bool>,
+    ) -> PyResult<&PyAny> {
+        future_into_py(py, async move {
+            let config = match profile_name {
+                Some(profile_name) => ClientConfiguration::load_profile(profile_name).await,
+                None => ClientConfiguration::load_default().await,
+            };
+
+            let client = config
+                .map(Qcs::with_config)
+                .map_err(LoadError)
+                .map_err(ToPythonError::to_py_err)?;
+
+            let client = match use_gateway {
+                None => client,
+                Some(use_gateway) => client.with_use_gateway(use_gateway),
+            };
+
+            Ok(Self(client))
+        })
+    }
+
+    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyObject {
+        match op {
+            CompareOp::Eq => (self == other).into_py(py),
+            CompareOp::Ne => (self != other).into_py(py),
+            _ => py.NotImplemented(),
+        }
+    }
+}
diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs
new file mode 100644
index 000000000..5037ceb22
--- /dev/null
+++ b/crates/python/src/qpu/mod.rs
@@ -0,0 +1,19 @@
+use pyo3::exceptions::PyRuntimeError;
+use rigetti_pyo3::{create_init_submodule, py_wrap_error, wrap_error};
+
+pub mod client;
+pub mod quilc;
+
+create_init_submodule! {
+    errors: [QcsIsaError],
+    submodules: [
+        "client": client::init_submodule,
+        "quilc": quilc::init_submodule
+    ],
+}
+
+wrap_error! {
+    IsaError(qcs::qpu::IsaError);
+}
+
+py_wrap_error!(qpu, IsaError, QcsIsaError, PyRuntimeError);
diff --git a/crates/python/src/qpu/quilc/mod.rs b/crates/python/src/qpu/quilc/mod.rs
new file mode 100644
index 000000000..9eb74f2aa
--- /dev/null
+++ b/crates/python/src/qpu/quilc/mod.rs
@@ -0,0 +1,41 @@
+use pyo3::{exceptions::PyRuntimeError, pymethods};
+use qcs::qpu::quilc::{CompilerOpts, TargetDevice, DEFAULT_COMPILER_TIMEOUT};
+use rigetti_pyo3::{
+    create_init_submodule, py_wrap_error, py_wrap_struct, py_wrap_type, wrap_error,
+};
+
+create_init_submodule! {
+    classes: [
+        PyCompilerOpts,
+        PyTargetDevice
+    ],
+    consts: [DEFAULT_COMPILER_TIMEOUT],
+    errors: [QuilcError],
+}
+
+py_wrap_type! {
+    #[derive(Default)]
+    PyCompilerOpts(CompilerOpts) as "CompilerOpts";
+}
+
+#[pymethods]
+impl PyCompilerOpts {
+    #[new]
+    #[args("/", timeout = "DEFAULT_COMPILER_TIMEOUT")]
+    pub fn new(timeout: Option<u8>) -> Self {
+        Self::from(CompilerOpts::new().with_timeout(timeout))
+    }
+
+    #[staticmethod]
+    #[allow(clippy::should_implement_trait)]
+    pub fn default() -> Self {
+        <Self as Default>::default()
+    }
+}
+
+wrap_error!(Error(qcs::qpu::quilc::Error));
+py_wrap_error!(quilc, Error, QuilcError, PyRuntimeError);
+
+py_wrap_struct! {
+    PyTargetDevice(TargetDevice) as "TargetDevice" {}
+}
diff --git a/crates/python/src/register_data.rs b/crates/python/src/register_data.rs
new file mode 100644
index 000000000..9f6fbc1ac
--- /dev/null
+++ b/crates/python/src/register_data.rs
@@ -0,0 +1,15 @@
+use pyo3::{
+    types::{PyComplex, PyFloat, PyInt},
+    Py,
+};
+use qcs::RegisterData;
+use rigetti_pyo3::py_wrap_union_enum;
+
+py_wrap_union_enum! {
+    PyRegisterData(RegisterData) as "RegisterData" {
+        i8: I8 => Vec<Vec<Py<PyInt>>>,
+        f64: F64 => Vec<Vec<Py<PyFloat>>>,
+        i16: I16 => Vec<Vec<Py<PyInt>>>,
+        complex32: Complex32 => Vec<Vec<Py<PyComplex>>>
+    }
+}


Test patch:
diff --git a/crates/python/tests/conftest.py b/crates/python/tests/conftest.py
index ce3ce3798..4614bc506 100644
--- a/crates/python/tests/conftest.py
+++ b/crates/python/tests/conftest.py
@@ -1,6 +1,17 @@
+import os
+
 import pytest
 
 
+TEST_CONFIG_DIR = os.path.join(
+    os.path.dirname(os.path.realpath(__file__)), "./qcs_config"
+)
+TEST_QCS_SETTINGS_PATH = os.path.join(TEST_CONFIG_DIR, "settings.toml")
+TEST_QCS_SECRETS_PATH = os.path.join(TEST_CONFIG_DIR, "secrets.toml")
+os.environ["QCS_SETTINGS_FILE_PATH"] = TEST_QCS_SETTINGS_PATH
+os.environ["QCS_SECRETS_FILE_PATH"] = TEST_QCS_SECRETS_PATH
+
+
 @pytest.fixture
 def device_2q() -> str:
     import json
diff --git a/crates/python/tests/qcs_config/secrets.toml b/crates/python/tests/qcs_config/secrets.toml
new file mode 100644
index 000000000..12c409e9f
--- /dev/null
+++ b/crates/python/tests/qcs_config/secrets.toml
@@ -0,0 +1,9 @@
+[credentials]
+
+  [credentials.default]
+
+    [credentials.default.token_payload]
+      access_token = "access"
+      id_token = ""
+      refresh_token = "refresh"
+      token_type = "Bearer"
diff --git a/crates/python/tests/qcs_config/settings.toml b/crates/python/tests/qcs_config/settings.toml
new file mode 100644
index 000000000..4d32338f9
--- /dev/null
+++ b/crates/python/tests/qcs_config/settings.toml
@@ -0,0 +1,24 @@
+default_profile_name = "default"
+
+[auth_servers]
+
+  [auth_servers.default]
+    client_id = "0oa3ykoirzDKpkfzk357"
+    issuer = "https://auth.qcs.rigetti.com/oauth2/aus8jcovzG0gW2TUG355"
+
+[profiles]
+
+  [profiles.default]
+    api_url = "https://api.qcs.rigetti.com"
+    auth_server_name = "default"
+    credentials_name = "default"
+
+  [profiles.empty]
+    api_url = "https://api.qcs.rigetti.com"
+    auth_server_name = "default"
+    credentials_name = "does-not-exist"
+
+  [profiles.broken]
+    api_url = "https://api.qcs.rigetti.com"
+    auth_server_name = "does-not-exist"
+    credentials_name = "default"
diff --git a/crates/python/tests/test_api.py b/crates/python/tests/test_api.py
index efa798656..eb0993991 100644
--- a/crates/python/tests/test_api.py
+++ b/crates/python/tests/test_api.py
@@ -1,11 +1,12 @@
 from unittest.mock import patch
 import pytest
 
+import re
+
 import qcs_sdk
 
 
 @pytest.mark.asyncio
-@pytest.mark.skip
 async def test_compile(native_bitflip_program: str, device_2q: str):
     await qcs_sdk.compile(native_bitflip_program, device_2q)
 
@@ -30,9 +31,10 @@ def test_rewrite_arithmetic():
     rewritten_arithmetic = qcs_sdk.rewrite_arithmetic(native_quil)
     expected = {
         "program": "DECLARE __SUBST REAL[1]\nRX(__SUBST[0]) 0\n",
-        "recalculation_table": ['((2*theta[0])/6.283185307179586)'],
+        "recalculation_table": ["((2*theta[0])/6.283185307179586)"],
     }
-    assert rewritten_arithmetic == expected
+    assert rewritten_arithmetic.program == expected["program"]
+    assert rewritten_arithmetic.recalculation_table == expected["recalculation_table"]
 
 
 def test_build_patch_values():
@@ -40,4 +42,27 @@ def test_build_patch_values():
     recalculation_table = ["0.5*theta[0]"]
     expected = {"theta": [2.0], "__SUBST": [1.0]}
     patch_values = qcs_sdk.build_patch_values(recalculation_table, memory)
-    assert patch_values == expected
\ No newline at end of file
+    assert patch_values == expected
+
+
+def test_exe_parameters():
+    """Should be able to construct and pass exe parameters"""
+    exe_parameter = qcs_sdk.ExeParameter("a", 1, 2.5)
+    assert exe_parameter.name == "a"
+    assert exe_parameter.index == 1
+    assert exe_parameter.value == 2.5
+
+    qcs_sdk.Executable("quil", parameters=[exe_parameter])
+
+
+@pytest.mark.asyncio
+async def test_get_quilc_version():
+    version = await qcs_sdk.get_quilc_version()
+    assert re.match(r"^([0-9]+)\.([0-9]+)\.([0-9]+)$", version)
+
+
+@pytest.mark.asyncio
+@pytest.mark.skip
+async def test_list_quantum_processors():
+    qpus = await qcs_sdk.list_quantum_processors()
+    assert isinstance(qpus, list)
diff --git a/crates/python/tests/test_client.py b/crates/python/tests/test_client.py
new file mode 100644
index 000000000..29552b395
--- /dev/null
+++ b/crates/python/tests/test_client.py
@@ -0,0 +1,45 @@
+import pytest
+
+from qcs_sdk import QcsClient
+from qcs_sdk.qpu.client import QcsLoadError, QcsClientAuthServer, QcsClientTokens
+
+@pytest.fixture
+def default_client():
+    return QcsClient()
+
+
+@pytest.mark.asyncio
+async def test_client_empty_profile_is_default(default_client: QcsClient):
+    """The profile "empty" is configured to be similar to a default client."""
+    client = await QcsClient.load(profile_name="empty")
+
+    assert client == default_client
+
+
+@pytest.mark.asyncio
+async def test_client_default_profile_is_not_empty(default_client: QcsClient):
+    """The "default" profile is configured to have a token, unlike the default client."""
+    client = await QcsClient.load()
+
+    assert client != default_client
+
+
+@pytest.mark.asyncio
+async def test_client_broken_raises():
+    """Using a profile with broken configuration should surface the underlying error."""
+    with pytest.raises(QcsLoadError, match=r"Expected auth server .* but it didn't exist"):
+        await QcsClient.load(profile_name="broken")
+
+
+def test_client_auth_server_can_be_manually_defined():
+    """Ensures that pyo3 usage is correct."""
+    auth_server = QcsClientAuthServer(client_id="foo", issuer="bar")
+    assert auth_server.client_id == "foo"
+    assert auth_server.issuer == "bar"
+
+
+def test_client_tokens_can_be_manually_defined():
+    """Ensures that pyo3 usage is correct."""
+    auth_server = QcsClientTokens(bearer_access_token="foo", refresh_token="bar")
+    assert auth_server.bearer_access_token == "foo"
+    assert auth_server.refresh_token == "bar"