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

Issue:
Setup distribution for python bindings
# Description

The goal of this PR is to implement a GitHub workflow that can easily build wheels that are compatible with various permutations of platforms, architectures, and python versions. 

## TODO

- [X] ~Create build wheels step for MacOS~ [Done](https://github.com/rigetti/qcs-sdk-rust/actions/runs/3017737018)
- [X] ~Create build wheels step for Linux~ [Done](https://github.com/rigetti/qcs-sdk-rust/actions/runs/3018358655) (without manylinux, see note below)
- [X] ~Create build wheels step for Linux on ARM archs~ [Done](https://github.com/rigetti/qcs-sdk-rust/actions/runs/3024379974)
- [ ] ~Create build wheels step for Windows~ (Punting this, see below)
- [X] ~Create build sdist step~ (part of macos step)
- [X] ~Create step to publish all wheels to TestPyPI~
- [X] ~Update release step to use `maturin upload`~ [Done](https://github.com/rigetti/qcs-sdk-rust/actions/runs/3019024434)
- [X] ~Update Python package level README.md~
- [X] ~Get metadata and README uploading to PyPI~ [Done](https://test.pypi.org/project/qcs-sdk-python/) (fixed the typos that are there, they'll be gone when we release to real PyPi)
- [X] ~Automate prereleases on merge to main and use tags/workflow-dispatch to trigger full releases~
- [X] ~Update release step to point to real PyPI~
- [X] Add documentation

## Notes

### manylinux and openssl

**Note**: leaving this here for posterity, but using the `rustls-tls` feature of `reqwest` allows us to build on manylinux images without any issue.

Our Rust code depends on `openssl-sys`, which in turn is a dependency of `reqwest`. The `openssl-sys` crate depends on development tools for OpenSSL to be installed, but the `manylinux` images don't have them. I am able to build on their image to install `openssl-devel` that enables these builds but:

1) This isn't possible with `messense/maturin-action@v1`, so we'd have to fork it or otherwise build our own action that did the same thing, but allowed the installation of additional packages on the manylinux image.

2) The implications of building wheels with openssl might compromise security of users. See [this comment](https://github.com/pypa/manylinux/issues/74#issuecomment-229795706) on why the manylinux maintainers don't simply add openssl to the image.

~For now, I have the wheels building directly on the linux host since it's easy enough to use apt to install `libssl-dev` before the build.~

Using the `rustls-tls` feature of `reqwest` solves the issue of (1) by eliminating the need for the openssl dependency. With that, I'm not sure how big of a concern (2) is anymore. Seems to me that it poses a similar risk to using any other third party package. 

### Windows
Builds on windows seem to be [failing](https://github.com/rigetti/qcs-sdk-rust/runs/8259691282?check_suite_focus=true) due to the `libzmq` package. In the interest of time, I'm going to create a followup issue to build Windows wheels. See #146 


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 1f619befc..4f9efaa3f 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -26,4 +26,4 @@ jobs:
           CARGO_MAKE_BUILD_EXAMPLES: true
         with:
           command: make
-          args: ci-flow
+          args: --cwd crates/lib --makefile Makefile.toml ci-flow
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
new file mode 100644
index 000000000..50eb357cb
--- /dev/null
+++ b/.github/workflows/prepare-release.yml
@@ -0,0 +1,40 @@
+name: Prepare Release
+
+on:
+  push:
+    branches:
+      - main
+  workflow_dispatch:
+    inputs:
+      type:
+        description: Bump versions and trigger a new release.
+        required: true
+        default: release
+        options:
+          - release
+          - prerelease
+
+jobs:
+  prepare-release:
+    runs-on: ubuntu-latest
+    env:
+      REMOTE_REPO: https://${{ github.actor }}:${{ github.token }}@github.com/${{ github.repository }}.git
+      GITHUB_TOKEN: ${{ github.token }}
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
+    - name: Install Knope
+      uses: knope-dev/action@v1
+      with:
+        version: 0.6.1 # Test before updating, breaking changes likely: https://github.com/knope-dev/action#install-latest-version
+    - run: |
+        git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+        git config user.name "github-actions[bot]"
+        git remote set-url origin "${REMOTE_REPO}"
+    - name: Prepare Prerelease
+      run: knope prerelease
+      if: github.event_name == 'push'
+    - name: Prepare Release
+      run: knope ${{ inputs.type }}
+      if: github.event_name == 'workflow_dispatch'
diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml
new file mode 100644
index 000000000..34fe71e29
--- /dev/null
+++ b/.github/workflows/release-python.yml
@@ -0,0 +1,90 @@
+name: Release Python
+
+on:
+  push:
+    tags:
+      - python/v*
+
+jobs:
+  macos:
+    runs-on: macos-11
+    strategy:
+      matrix:
+        python-version: ['3.7', '3.8', '3.9', '3.10']
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install Rust
+        uses: actions-rs/toolchain@v1
+        if: matrix.python-version != '3.7'
+        with:
+          toolchain: stable
+          target: aarch64-apple-darwin
+          profile: minimal
+          default: true
+      - name: Build wheels - x86_64
+        # Python 3.7 does not support aarch64 so we only build it for x86_64
+        if: matrix.python-version == '3.7'
+        uses: messense/maturin-action@v1
+        with:
+          target: x86_64
+          args: -i python --release --manifest-path crates/python/Cargo.toml --out dist --sdist
+      - name: Build wheels - universal2
+        # universal2 supports both x86_64 and aarch64 so every Python > 3.7 only needs this wheel
+        if: matrix.python-version != '3.7'
+        uses: messense/maturin-action@v1
+        with:
+          args: -i python --release --universal2 --manifest-path crates/python/Cargo.toml --out dist
+      - name: Install wheel
+        run: |
+          pip install qcs-sdk-python --no-index --find-links dist --force-reinstall
+      - name: Upload wheels
+        uses: actions/upload-artifact@v2
+        with:
+          name: wheels
+          path: dist
+  linux:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ['3.7', '3.8', '3.9', '3.10']
+        target: [x86_64, aarch64]
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install Rust
+      uses: actions-rs/toolchain@v1
+      with:
+        toolchain: stable
+        profile: minimal
+        default: true
+    - uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Build wheels
+      uses: messense/maturin-action@v1
+      with:
+        target: ${{ matrix.target }}
+        manylinux: 2014
+        args: -i ${{ matrix.python-version }} --release --manifest-path crates/python/Cargo.toml --out dist
+    - name: Upload wheels
+      uses: actions/upload-artifact@v2
+      with:
+        name: wheels
+        path: dist
+
+  release:
+    name: Release
+    runs-on: ubuntu-latest
+    needs: [ macos, linux ]
+    steps:
+      - uses: actions/download-artifact@v2
+      - name: Publish to PyPi
+        env:
+          MATURIN_USERNAME: ${{ secrets.PYPI_USERNAME }}
+          MATURIN_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+        uses: messense/maturin-action@v1
+        with:
+          command: upload 
+          args: --skip-existing wheels/*
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7181b9076..06af0767f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,13 +12,9 @@ jobs:
           fetch-depth: 0
           token: ${{ secrets.PAT }}
       - name: Install Knope
-        run: |
-          wget https://github.com/knope-dev/knope/releases/download/$KNOPE_VERSION/knope-x86_64-unknown-linux-musl-$KNOPE_VERSION.tgz
-          tar -xvf knope-x86_64-unknown-linux-musl-$KNOPE_VERSION.tgz
-          cp knope-x86_64-unknown-linux-musl-$KNOPE_VERSION/knope $HOME/.cargo/bin
-          rm -rf knope-x86_64-unknown-linux-musl-${KNOPE_VERSION}*
-        env:
-          KNOPE_VERSION: v0.4.1
+        uses: knope-dev/action@v1
+        with:
+          version: 0.6.1 # Test before updating, breaking changes likely: https://github.com/knope-dev/action#install-latest-version
       - name: Configure Git
         run: |
           git config --global user.name "${{ github.triggering_actor }}"
diff --git a/Cargo.lock b/Cargo.lock
index 42e871743..c14d473e7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -42,9 +42,9 @@ dependencies = [
 
 [[package]]
 name = "block-buffer"
-version = "0.10.2"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
 dependencies = [
  "generic-array",
 ]
@@ -61,9 +61,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.10.0"
+version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
 
 [[package]]
 name = "byteorder"
@@ -73,9 +73,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "bytes"
-version = "1.1.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
 
 [[package]]
 name = "cc"
@@ -98,36 +98,20 @@ dependencies = [
  "cc",
 ]
 
-[[package]]
-name = "core-foundation"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "core-foundation-sys"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
-
 [[package]]
 name = "cpufeatures"
-version = "0.2.2"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "crypto-common"
-version = "0.1.3"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
  "typenum",
@@ -148,7 +132,7 @@ version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
 dependencies = [
- "block-buffer 0.10.2",
+ "block-buffer 0.10.3",
  "crypto-common",
 ]
 
@@ -195,9 +179,9 @@ dependencies = [
 
 [[package]]
 name = "erased-serde"
-version = "0.3.22"
+version = "0.3.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "003000e712ad0f95857bd4d2ef8d1890069e06554101697d12050668b2f6f020"
+checksum = "54558e0ba96fbe24280072642eceb9d7d442e32c7ec0ea9e7ecd7b4ea2cf4e11"
 dependencies = [
  "serde",
 ]
@@ -210,9 +194,9 @@ checksum = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8"
 
 [[package]]
 name = "fastrand"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
 dependencies = [
  "instant",
 ]
@@ -238,36 +222,20 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
-[[package]]
-name = "foreign-types"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
-
-[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
-
 [[package]]
 name = "form_urlencoded"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
 dependencies = [
- "matches",
  "percent-encoding",
 ]
 
 [[package]]
 name = "futures"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
+checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -280,9 +248,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
+checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -290,15 +258,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
+checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
+checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -307,15 +275,15 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
+checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
 
 [[package]]
 name = "futures-macro"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
+checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -324,21 +292,21 @@ dependencies = [
 
 [[package]]
 name = "futures-sink"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
+checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
 
 [[package]]
 name = "futures-task"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
+checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
 
 [[package]]
 name = "futures-util"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
+checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -354,9 +322,9 @@ dependencies = [
 
 [[package]]
 name = "generic-array"
-version = "0.14.5"
+version = "0.14.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
 dependencies = [
  "typenum",
  "version_check",
@@ -364,20 +332,20 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
 dependencies = [
  "cfg-if",
  "libc",
- "wasi 0.10.2+wasi-snapshot-preview1",
+ "wasi",
 ]
 
 [[package]]
 name = "h2"
-version = "0.3.13"
+version = "0.3.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57"
+checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
 dependencies = [
  "bytes",
  "fnv",
@@ -388,21 +356,21 @@ dependencies = [
  "indexmap",
  "slab",
  "tokio",
- "tokio-util 0.7.2",
+ "tokio-util 0.7.4",
  "tracing",
 ]
 
 [[package]]
 name = "hashbrown"
-version = "0.12.1"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
 [[package]]
 name = "headers"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d"
+checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
 dependencies = [
  "base64",
  "bitflags",
@@ -411,7 +379,7 @@ dependencies = [
  "http",
  "httpdate",
  "mime",
- "sha-1 0.10.0",
+ "sha1",
 ]
 
 [[package]]
@@ -446,9 +414,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 
 [[package]]
 name = "http"
-version = "0.2.7"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
 dependencies = [
  "bytes",
  "fnv",
@@ -468,9 +436,9 @@ dependencies = [
 
 [[package]]
 name = "httparse"
-version = "1.7.1"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
 
 [[package]]
 name = "httpdate"
@@ -480,9 +448,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
 
 [[package]]
 name = "hyper"
-version = "0.14.19"
+version = "0.14.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f"
+checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -515,26 +483,12 @@ dependencies = [
  "tokio-rustls",
 ]
 
-[[package]]
-name = "hyper-tls"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
-dependencies = [
- "bytes",
- "hyper",
- "native-tls",
- "tokio",
- "tokio-native-tls",
-]
-
 [[package]]
 name = "idna"
-version = "0.2.3"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
 dependencies = [
- "matches",
  "unicode-bidi",
  "unicode-normalization",
 ]
@@ -549,6 +503,12 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "indoc"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -566,15 +526,15 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
 
 [[package]]
 name = "itoa"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
 
 [[package]]
 name = "js-sys"
-version = "0.3.57"
+version = "0.3.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -660,9 +620,19 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.126"
+version = "0.2.132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+
+[[package]]
+name = "lock_api"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
+checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
 
 [[package]]
 name = "log"
@@ -679,17 +649,11 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
 
-[[package]]
-name = "matches"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
-
 [[package]]
 name = "memchr"
-version = "2.3.4"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
 [[package]]
 name = "metadeps"
@@ -726,13 +690,13 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "mio"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
+checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
 dependencies = [
  "libc",
  "log",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi",
  "windows-sys",
 ]
 
@@ -754,24 +718,6 @@ dependencies = [
  "twoway",
 ]
 
-[[package]]
-name = "native-tls"
-version = "0.2.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
-dependencies = [
- "lazy_static",
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework",
- "security-framework-sys",
- "tempfile",
-]
-
 [[package]]
 name = "nom"
 version = "7.1.1"
@@ -809,9 +755,9 @@ dependencies = [
 
 [[package]]
 name = "num-complex"
-version = "0.4.1"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97fbc387afefefd5e9e39493299f3069e14a140dd34dc19b4c1c1a8fddb6a790"
+checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
 dependencies = [
  "num-traits",
 ]
@@ -839,9 +785,9 @@ dependencies = [
 
 [[package]]
 name = "num-rational"
-version = "0.4.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
 dependencies = [
  "autocfg",
  "num-bigint",
@@ -870,9 +816,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.12.0"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
+checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
 
 [[package]]
 name = "opaque-debug"
@@ -881,61 +827,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
-name = "openssl"
-version = "0.10.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
-dependencies = [
- "bitflags",
- "cfg-if",
- "foreign-types",
- "libc",
- "once_cell",
- "openssl-macros",
- "openssl-sys",
-]
-
-[[package]]
-name = "openssl-macros"
-version = "0.1.0"
+name = "parking_lot"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "lock_api",
+ "parking_lot_core",
 ]
 
 [[package]]
-name = "openssl-probe"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
-
-[[package]]
-name = "openssl-sys"
-version = "0.9.74"
+name = "parking_lot_core"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
+checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
 dependencies = [
- "autocfg",
- "cc",
+ "cfg-if",
  "libc",
- "pkg-config",
- "vcpkg",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
 ]
 
 [[package]]
 name = "paste"
-version = "1.0.7"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
+checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
 
 [[package]]
 name = "percent-encoding"
-version = "2.1.0"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
 
 [[package]]
 name = "petgraph"
@@ -949,18 +873,18 @@ dependencies = [
 
 [[package]]
 name = "pin-project"
-version = "1.0.11"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.0.11"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -993,13 +917,95 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.39"
+version = "1.0.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
 dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "pyo3"
+version = "0.16.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0220c44442c9b239dd4357aa856ac468a4f5e1f0df19ddb89b2522952eb4c6ca"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "parking_lot",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-asyncio"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb097197d88c0d24df47fcf43c1f7eadde27b17a558cf8875123d60cee2cbba8"
+dependencies = [
+ "futures",
+ "once_cell",
+ "pin-project-lite",
+ "pyo3",
+ "tokio",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.16.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c819d397859445928609d0ec5afc2da5204e0d0f73d6bf9e153b04e83c9cdc2"
+dependencies = [
+ "once_cell",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.16.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca882703ab55f54702d7bfe1189b41b0af10272389f04cae38fe4cd56c65f75f"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.16.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "568749402955ad7be7bad9a09b8593851cd36e549ac90bfd44079cea500f3f21"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.16.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "611f64e82d98f447787e82b8e7b0ebc681e1eb78fc1252668b2c605ffb4e1eb8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pythonize"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af9dc8ab903e422af4752dc4ad0d176147a71c9c1c982e1a513ba7a6438c1635"
+dependencies = [
+ "pyo3",
+ "serde",
+]
+
 [[package]]
 name = "qcs"
 version = "0.6.0"
@@ -1045,6 +1051,20 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "qcs-sdk-python"
+version = "0.1.4"
+dependencies = [
+ "pyo3",
+ "pyo3-asyncio",
+ "pyo3-build-config",
+ "pythonize",
+ "qcs",
+ "qcs-api",
+ "serde_json",
+ "tokio",
+]
+
 [[package]]
 name = "quick-error"
 version = "1.2.3"
@@ -1068,9 +1088,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.18"
+version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
 dependencies = [
  "proc-macro2",
 ]
@@ -1107,9 +1127,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.2.13"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
 dependencies = [
  "bitflags",
 ]
@@ -1150,14 +1170,12 @@ dependencies = [
  "http-body",
  "hyper",
  "hyper-rustls",
- "hyper-tls",
  "ipnet",
  "js-sys",
  "lazy_static",
  "log",
  "mime",
  "mime_guess",
- "native-tls",
  "percent-encoding",
  "pin-project-lite",
  "rustls",
@@ -1166,7 +1184,6 @@ dependencies = [
  "serde_json",
  "serde_urlencoded",
  "tokio",
- "tokio-native-tls",
  "tokio-rustls",
  "tower-service",
  "url",
@@ -1228,18 +1245,18 @@ dependencies = [
 
 [[package]]
 name = "rustls-pemfile"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9"
+checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
 dependencies = [
  "base64",
 ]
 
 [[package]]
 name = "ryu"
-version = "1.0.10"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
 
 [[package]]
 name = "safemem"
@@ -1247,22 +1264,18 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
 
-[[package]]
-name = "schannel"
-version = "0.1.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
-dependencies = [
- "lazy_static",
- "windows-sys",
-]
-
 [[package]]
 name = "scoped-tls"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
 
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
 [[package]]
 name = "sct"
 version = "0.7.0"
@@ -1273,34 +1286,11 @@ dependencies = [
  "untrusted",
 ]
 
-[[package]]
-name = "security-framework"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
-dependencies = [
- "bitflags",
- "core-foundation",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework-sys"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
 [[package]]
 name = "serde"
-version = "1.0.143"
+version = "1.0.144"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553"
+checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
 dependencies = [
  "serde_derive",
 ]
@@ -1316,9 +1306,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.143"
+version = "1.0.144"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391"
+checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1327,9 +1317,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.83"
+version = "1.0.85"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
 dependencies = [
  "itoa",
  "ryu",
@@ -1362,21 +1352,30 @@ dependencies = [
 ]
 
 [[package]]
-name = "sha-1"
-version = "0.10.0"
+name = "sha1"
+version = "0.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
+checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549"
 dependencies = [
  "cfg-if",
  "cpufeatures",
  "digest 0.10.3",
 ]
 
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "simple_logger"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "166fea527c36d9b8a0a88c0c6d4c5077c699d9ffb5cf890b231a3f08c35f3d40"
+checksum = "48047e77b528151aaf841a10a9025f9459da80ba820e425ff7eb005708a76dc7"
 dependencies = [
  "atty",
  "log",
@@ -1385,15 +1384,24 @@ dependencies = [
 
 [[package]]
 name = "slab"
-version = "0.4.6"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
+checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
 
 [[package]]
 name = "socket2"
-version = "0.4.4"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
 dependencies = [
  "libc",
  "winapi",
@@ -1413,15 +1421,21 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[package]]
 name = "syn"
-version = "1.0.95"
+version = "1.0.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
+checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
 dependencies = [
  "proc-macro2",
  "quote",
  "unicode-ident",
 ]
 
+[[package]]
+name = "target-lexicon"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1"
+
 [[package]]
 name = "tempfile"
 version = "3.3.0"
@@ -1438,18 +1452,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.32"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
+checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.32"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
+checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1473,9 +1487,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.20.1"
+version = "1.21.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
+checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95"
 dependencies = [
  "autocfg",
  "bytes",
@@ -1484,7 +1498,9 @@ dependencies = [
  "mio",
  "num_cpus",
  "once_cell",
+ "parking_lot",
  "pin-project-lite",
+ "signal-hook-registry",
  "socket2",
  "tokio-macros",
  "winapi",
@@ -1492,25 +1508,15 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
+checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn",
 ]
 
-[[package]]
-name = "tokio-native-tls"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
-dependencies = [
- "native-tls",
- "tokio",
-]
-
 [[package]]
 name = "tokio-rustls"
 version = "0.23.4"
@@ -1524,9 +1530,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-stream"
-version = "0.1.8"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
+checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9"
 dependencies = [
  "futures-core",
  "pin-project-lite",
@@ -1562,9 +1568,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.7.2"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c"
+checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
 dependencies = [
  "bytes",
  "futures-core",
@@ -1591,41 +1597,29 @@ dependencies = [
 
 [[package]]
 name = "tower-service"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
 
 [[package]]
 name = "tracing"
-version = "0.1.34"
+version = "0.1.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
+checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
 dependencies = [
  "cfg-if",
  "log",
  "pin-project-lite",
- "tracing-attributes",
  "tracing-core",
 ]
 
-[[package]]
-name = "tracing-attributes"
-version = "0.1.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "tracing-core"
-version = "0.1.26"
+version = "0.1.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f"
+checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
 dependencies = [
- "lazy_static",
+ "once_cell",
 ]
 
 [[package]]
@@ -1647,7 +1641,7 @@ dependencies = [
  "httparse",
  "log",
  "rand",
- "sha-1 0.9.8",
+ "sha-1",
  "thiserror",
  "url",
  "utf-8",
@@ -1685,19 +1679,25 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.0"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
 
 [[package]]
 name = "unicode-normalization"
-version = "0.1.19"
+version = "0.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
 dependencies = [
  "tinyvec",
 ]
 
+[[package]]
+name = "unindent"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58ee9362deb4a96cef4d437d1ad49cffc9b9e92d202b6995674e928ce684f112"
+
 [[package]]
 name = "untrusted"
 version = "0.7.1"
@@ -1706,13 +1706,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 
 [[package]]
 name = "url"
-version = "2.2.2"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
 dependencies = [
  "form_urlencoded",
  "idna",
- "matches",
  "percent-encoding",
 ]
 
@@ -1731,12 +1730,6 @@ dependencies = [
  "getrandom",
 ]
 
-[[package]]
-name = "vcpkg"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-
 [[package]]
 name = "version_check"
 version = "0.9.4"
@@ -1783,12 +1776,6 @@ dependencies = [
  "tracing",
 ]
 
-[[package]]
-name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
-
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
@@ -1797,9 +1784,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.80"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -1807,13 +1794,13 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.80"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
 dependencies = [
  "bumpalo",
- "lazy_static",
  "log",
+ "once_cell",
  "proc-macro2",
  "quote",
  "syn",
@@ -1822,9 +1809,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.30"
+version = "0.4.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2"
+checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -1834,9 +1821,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.80"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -1844,9 +1831,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.80"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1857,15 +1844,15 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.80"
+version = "0.2.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
 
 [[package]]
 name = "web-sys"
-version = "0.3.57"
+version = "0.3.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -1883,9 +1870,9 @@ dependencies = [
 
 [[package]]
 name = "webpki-roots"
-version = "0.22.3"
+version = "0.22.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf"
+checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
 dependencies = [
  "webpki",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 92da9b738..4df366c7a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,43 +1,2 @@
-[package]
-name = "qcs"
-description = "High level interface for running Quil on a QPU"
-version = "0.6.0"
-edition = "2018"
-license = "Apache-2.0"
-repository = "https://github.com/rigetti/qcs-sdk-rust"
-keywords = ["pyquil", "SDK", "Rigetti", "Quil", "Quantum"]
-categories = ["api-bindings", "compilers", "science", "emulators"]
-
-[features]
-manual-tests = []
-
-[dependencies]
-dirs = "4.0.0"
-enum-as-inner = "0.5.1"
-futures = "0.3.21"
-indexmap = "1.9.1"
-lazy_static = "1.4.0"
-log = "0.4.17"
-num = "0.4.0"
-qcs-api = "0.2.1"
-quil-rs = "0.12.2"
-reqwest = { version = "0.11.11", features = ["rustls", "json"] }
-rmp-serde = "1.1.0"
-serde = { version = "1.0.143", features = ["derive"] }
-serde_bytes = "0.11.7"
-serde_json = "1.0.83"
-thiserror = "1.0.32"
-tokio = { version = "1.20.1", features = ["fs"] }
-toml = "0.5.9"
-uuid = { version = "1.1.2", features = ["v4"] }
-zmq = { version = "0.9.2", features = ["vendored"] }
-
-[dev-dependencies]
-erased-serde = "0.3.22"
-float-cmp = "0.9.0"
-hex = "0.4.3"
-maplit = "1.0.2"
-simple_logger = { version = "2.2.0", default-features = false }
-tempfile = "3.3.0"
-tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] }
-warp = "0.3.2"
+[workspace]
+members = ["crates/*"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 9631b7e03..08ade0b39 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
-# qcs-sdk-rust
+# QCS Rust SDK
 
-The `qcs` crate is a high-level interface to Rigetti's [Quantum Cloud Services], allowing Rust developers to run [Quil] programs on Rigetti's [QPUs]. This crate is a Rust port of [`pyQuil`], though it currently has a much smaller feature set.
+This repo provides two crates:
 
-> For the C-bindings to this library, check out [qcs-sdk-c](https://github.com/rigetti/qcs-sdk-c)
+- `qcs` which is the Rust SDK for executing quantum programs on Rigetti QPUs; and
+- `qcs-sdk-python` which defines, generates, and publishes the Python bindgins
+  to make use of the Rust SDK.
 
 ## Documentation
 
@@ -14,6 +16,15 @@ Most development tasks are automated with [cargo-make] (like make, but you can h
 
 In order to run all checks exactly the same way that CI does, use `makers ci-flow` from the project root (workspace).
 
+
+### Commits
+
+Commits should follow the conventional commit syntax, with one of the following [scopes][scopes]:
+
+- `lib` or `rust`: changes to the rust SDK
+- `python`: changes to the Python bindings
+- No scope: changes to both crates
+
 ### Dependencies
 
 Because this library relies on [ØMQ], [`cmake`] is required:
@@ -38,7 +49,9 @@ To build the docs.rs-style docs, run `makers docs`. You can also do `makers serv
 
 ## Release
 
-To release this crate, manually run the `release` workflow in GitHub Actions.
+To release the library crate or the bindings to Python, manually run the `release` or `release python` workflow in GitHub Actions, respectively.
+
+Pre-releases of the Python package happen automatically on merge to main.
 
 [cargo-make]: https://sagiegurari.github.io/cargo-make/
 [Quantum Cloud Services]: https://docs.rigetti.com/qcs/
@@ -46,4 +59,5 @@ To release this crate, manually run the `release` workflow in GitHub Actions.
 [QPUs]: https://qcs.rigetti.com/qpus/
 [`pyQuil`]: https://github.com/rigetti/pyquil
 [rustdoc]: https://doc.rust-lang.org/rustdoc/index.html
-[docs.rs]: https://docs.rs/qcs
\ No newline at end of file
+[docs.rs]: https://docs.rs/qcs
+[scopes]: https://www.conventionalcommits.org/en/v1.0.0/#commit-message-with-scope
diff --git a/CHANGELOG.md b/crates/lib/CHANGELOG.md
similarity index 100%
rename from CHANGELOG.md
rename to crates/lib/CHANGELOG.md
diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml
new file mode 100644
index 000000000..f96635de7
--- /dev/null
+++ b/crates/lib/Cargo.toml
@@ -0,0 +1,44 @@
+[package]
+name = "qcs"
+description = "High level interface for running Quil on a QPU"
+version = "0.6.0"
+edition = "2018"
+license = "Apache-2.0"
+repository = "https://github.com/rigetti/qcs-sdk-rust"
+keywords = ["pyquil", "SDK", "Rigetti", "Quil", "Quantum"]
+categories = ["api-bindings", "compilers", "science", "emulators"]
+readme = "./README.md"
+
+[features]
+manual-tests = []
+
+[dependencies]
+dirs = "4.0.0"
+enum-as-inner = "0.5.1"
+futures = "0.3.21"
+indexmap = "1.9.1"
+lazy_static = "1.4.0"
+log = "0.4.17"
+num = "0.4.0"
+qcs-api = "0.2.1"
+quil-rs = "0.12.2"
+reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "json"] }
+rmp-serde = "1.1.0"
+serde = { version = "1.0.143", features = ["derive"] }
+serde_bytes = "0.11.7"
+serde_json = "1.0.83"
+thiserror = "1.0.32"
+tokio = { version = "1.20.1", features = ["fs"] }
+toml = "0.5.9"
+uuid = { version = "1.1.2", features = ["v4"] }
+zmq = { version = "0.9.2", features = ["vendored"] }
+
+[dev-dependencies]
+erased-serde = "0.3.22"
+float-cmp = "0.9.0"
+hex = "0.4.3"
+maplit = "1.0.2"
+simple_logger = { version = "2.2.0", default-features = false }
+tempfile = "3.3.0"
+tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] }
+warp = "0.3.2"
diff --git a/Makefile.toml b/crates/lib/Makefile.toml
similarity index 100%
rename from Makefile.toml
rename to crates/lib/Makefile.toml
diff --git a/crates/lib/README.md b/crates/lib/README.md
new file mode 100644
index 000000000..cc2001362
--- /dev/null
+++ b/crates/lib/README.md
@@ -0,0 +1,49 @@
+# qcs-sdk-rust
+
+The `qcs` crate is a high-level interface to Rigetti's [Quantum Cloud Services], allowing Rust developers to run [Quil] programs on Rigetti's [QPUs]. This crate is a Rust port of [`pyQuil`], though it currently has a much smaller feature set.
+
+> For the C-bindings to this library, check out [qcs-sdk-c](https://github.com/rigetti/qcs-sdk-c)
+
+## Documentation
+
+This crate is documented primarily via [rustdoc] comments and examples, which are available on [docs.rs].
+
+## Development
+
+Most development tasks are automated with [cargo-make] (like make, but you can have dependencies on other Rust tools and a _ton_ of useful tasks are built in). Install cargo-make by doing `cargo install cargo-make`. Then you can invoke it with either `cargo make <task>` or `makers <task>`. Tasks are defined in files called `Makefile.toml`.
+
+In order to run all checks exactly the same way that CI does, use `makers ci-flow` from the project root (workspace).
+
+### Dependencies
+
+Because this library relies on [ØMQ], [`cmake`] is required:
+
+- macOS [Homebrew] : `brew install cmake`
+- Windows [Chocolatey]: `choco install cmake`
+- Debian: `apt install cmake`
+
+### Running Tests
+
+The best way to go about this is via `makers` or `cargo make` with no task. This will default to `dev-test-flow` which formats all code, builds, and tests everything.
+
+Any tests which cannot be run in CI should be run with `makers manual`. These tests require configured QCS credentials with access to internal functions, as well as a connection to the Rigetti VPN.
+
+### Linting
+
+`makers lint` will lint run all static checks.
+
+### Documentation
+
+To build the docs.rs-style docs, run `makers docs`. You can also do `makers serve-docs` to launch a local webserver for viewing immediately.
+
+## Release
+
+To release this crate, manually run the `release` workflow in GitHub Actions.
+
+[cargo-make]: https://sagiegurari.github.io/cargo-make/
+[quantum cloud services]: https://docs.rigetti.com/qcs/
+[quil]: https://github.com/quil-lang/quil
+[qpus]: https://qcs.rigetti.com/qpus/
+[`pyquil`]: https://github.com/rigetti/pyquil
+[rustdoc]: https://doc.rust-lang.org/rustdoc/index.html
+[docs.rs]: https://docs.rs/qcs
diff --git a/examples/delayed_job_retrieval.rs b/crates/lib/examples/delayed_job_retrieval.rs
similarity index 100%
rename from examples/delayed_job_retrieval.rs
rename to crates/lib/examples/delayed_job_retrieval.rs
diff --git a/crates/lib/examples/execute.rs b/crates/lib/examples/execute.rs
new file mode 100644
index 000000000..61e495eef
--- /dev/null
+++ b/crates/lib/examples/execute.rs
@@ -0,0 +1,27 @@
+use std::f64::consts::PI;
+
+use qcs::Executable;
+
+const PROGRAM: &str = r#"
+DECLARE ro BIT[2]
+DECLARE theta REAL
+RX(theta) 0
+X 0
+CNOT 0 1
+MEASURE 0 ro[0]
+MEASURE 1 ro[1]
+"#;
+
+#[tokio::main]
+async fn main() {
+    let mut exe = Executable::from_quil(PROGRAM);
+    let theta = PI;
+
+    let result = exe
+        .with_parameter("theta", 0, theta)
+        .execute_on_qpu("Aspen-11")
+        .await
+        .expect("Executed program on QPU");
+
+    dbg!(&result);
+}
diff --git a/examples/parametric_compilation.rs b/crates/lib/examples/parametric_compilation.rs
similarity index 96%
rename from examples/parametric_compilation.rs
rename to crates/lib/examples/parametric_compilation.rs
index e6bef8270..1e9136296 100644
--- a/examples/parametric_compilation.rs
+++ b/crates/lib/examples/parametric_compilation.rs
@@ -31,7 +31,7 @@ async fn main() {
             .with_parameter("theta", 0, theta)
             .execute_on_qpu("Aspen-11")
             .await
-            .expect("Failed to execute");
+            .expect("Executed program on QPU");
         total_execution_time += data
             .duration
             .expect("Aspen-11 should always report duration");
diff --git a/examples/quil_t.rs b/crates/lib/examples/quil_t.rs
similarity index 89%
rename from examples/quil_t.rs
rename to crates/lib/examples/quil_t.rs
index 32855f6e1..ab64dbf1b 100644
--- a/examples/quil_t.rs
+++ b/crates/lib/examples/quil_t.rs
@@ -21,10 +21,10 @@ async fn main() {
         .compile_with_quilc(false)
         .execute_on_qpu("Aspen-11")
         .await
-        .expect("Failed to execute")
+        .expect("Executed program on QPU")
         .registers
         .remove("ro")
-        .expect("Missing ro register");
+        .expect("Found ro register");
 
     assert!(result.as_i16().is_some());
 }
diff --git a/crates/lib/src/api.rs b/crates/lib/src/api.rs
new file mode 100644
index 000000000..e40ca21ca
--- /dev/null
+++ b/crates/lib/src/api.rs
@@ -0,0 +1,255 @@
+//! This module provides a small interface for doing compilation, translation,
+//! and execution.
+
+use log::warn;
+use quil_rs::Program;
+use serde::Serialize;
+
+use crate::{
+    configuration::Configuration,
+    qpu::{
+        self, engagement,
+        quilc::{self, TargetDevice},
+        rewrite_arithmetic::{self, Substitutions},
+        rpcq::Client,
+        runner::{self, JobId},
+        translation,
+    },
+};
+use std::{collections::HashMap, convert::TryFrom};
+
+/// Uses quilc to convert a Quil program to native Quil
+///
+/// # Errors
+///
+/// See [`quilc::compile_program`].
+pub fn compile(
+    quil: &str,
+    target_device: TargetDevice,
+    config: &Configuration,
+) -> Result<String, Box<dyn std::error::Error>> {
+    quilc::compile_program(quil, target_device, config)
+        .map_err(std::convert::Into::into)
+        .map(String::from)
+}
+
+/// 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)]
+pub struct RewriteArithmeticResult {
+    /// The rewritten program
+    pub program: String,
+    /// The expressions used to fill-in the `__SUBST` memory location. The
+    /// expression index in this vec is the same as that in `__SUBST`.
+    pub recalculation_table: Vec<String>,
+}
+
+/// Rewrite parametric arithmetic such that all gate parameters are only memory
+/// references to newly declared memory location (`__SUBST`).
+///
+/// A "recalculation" table is provided which can be used to populate the memory
+/// when needed (see `build_patch_values`).
+///
+/// # Errors
+///
+/// May return an error if the program fails to parse, or the parameter arithmetic
+/// cannot be rewritten.
+pub fn rewrite_arithmetic(native_quil: &str) -> Result<RewriteArithmeticResult, String> {
+    let program: Program = native_quil.parse()?;
+
+    let (program, subs) =
+        qpu::rewrite_arithmetic::rewrite_arithmetic(program).map_err(|e| e.to_string())?;
+    let recalculation_table = subs.into_iter().map(|expr| expr.to_string()).collect();
+
+    Ok(RewriteArithmeticResult {
+        program: program.to_string(true),
+        recalculation_table,
+    })
+}
+
+/// The result of a call to [`translate`] which provides information about the
+/// translated program.
+#[derive(Clone, Debug, PartialEq, Default, Serialize)]
+pub struct TranslationResult {
+    /// The memory defined in the program.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub memory_descriptors:
+        Option<::std::collections::HashMap<String, qcs_api::models::ParameterSpec>>,
+    /// The translated program.
+    pub program: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    /// The memory locations used for readout.
+    pub ro_sources: Option<Vec<Vec<String>>>,
+    /// ISO8601 timestamp of the settings used to translate the program. Translation is deterministic; a program translated twice with the same settings by the same version of the service will have identical output.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub settings_timestamp: Option<String>,
+}
+
+/// Translates a native Quil program into an executable
+///
+/// # Errors
+///
+/// Returns a [`translation::Error`] if translation fails.
+pub async fn translate(
+    native_quil: &str,
+    shots: u16,
+    quantum_processor_id: &str,
+    config: &Configuration,
+) -> Result<TranslationResult, translation::Error> {
+    let translation = translation::translate(
+        qpu::RewrittenQuil(native_quil.to_string()),
+        shots,
+        quantum_processor_id,
+        config,
+    )
+    .await?;
+
+    Ok(TranslationResult {
+        memory_descriptors: translation.memory_descriptors,
+        program: translation.program,
+        ro_sources: translation.ro_sources,
+        settings_timestamp: translation.settings_timestamp,
+    })
+}
+
+/// Submits an executable `program` to be run on the specified QPU
+///
+/// # Errors
+///
+/// May return an error if
+/// * an engagement is not available
+/// * an RPCQ client cannot be built
+/// * the program cannot be submitted
+#[allow(clippy::implicit_hasher)]
+pub async fn submit(
+    program: &str,
+    patch_values: HashMap<String, Vec<f64>>,
+    quantum_processor_id: &str,
+    config: &Configuration,
+) -> Result<String, runner::Error> {
+    // Is there a better way to map these patch_values keys? This
+    // negates the whole purpose of [`submit`] using `Box<str>`,
+    // instead of `String` directly, which normally would decrease
+    // copies _and_ require less space, since str can't be extended.
+    let patch_values = patch_values
+        .into_iter()
+        .map(|(k, v)| (k.into_boxed_str(), v))
+        .collect();
+    let engagement = engagement::get(String::from(quantum_processor_id), config)
+        .await
+        .map_err(|e| runner::Error::Qpu(format!("Unable to get engagement: {:?}", e)))?;
+    let rpcq_client = Client::try_from(&engagement).map_err(|e| {
+        warn!("Unable to connect to QPU via RPCQ: {:?}", e);
+        runner::Error::Connection(e)
+    })?;
+
+    let job_id = runner::submit(program, &patch_values, &rpcq_client)?;
+
+    Ok(job_id.0)
+}
+
+/// Evaluate the expressions in `recalculation_table` using the numeric values
+/// provided in `memory`.
+///
+/// # Errors
+#[allow(clippy::implicit_hasher)]
+pub fn build_patch_values(
+    recalculation_table: &[String],
+    memory: &HashMap<Box<str>, Vec<f64>>,
+) -> Result<HashMap<Box<str>, Vec<f64>>, String> {
+    let substitutions: Substitutions = recalculation_table
+        .iter()
+        .map(|expr| expr.parse())
+        .collect::<Result<_, _>>()
+        .map_err(|e| format!("Unable to interpret recalc table: {:?}", e))?;
+    rewrite_arithmetic::get_substitutions(&substitutions, memory)
+}
+
+/// A 64-bit complex number.
+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)]
+#[serde(untagged)] // Don't include the discriminant name in serialized output.
+pub enum Register {
+    /// A register of 64-bit floating point numbers
+    F64(Vec<f64>),
+    /// A register of 16-bit integers
+    I16(Vec<i16>),
+    /// A register of 64-bit complex numbers
+    Complex64(Vec<Complex64>),
+    /// A register of 8-bit integers (bytes)
+    I8(Vec<i8>),
+}
+
+impl From<qpu::runner::Register> for Register {
+    fn from(register: qpu::runner::Register) -> Self {
+        match 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())
+            }
+            runner::Register::I8(i) => Register::I8(i),
+        }
+    }
+}
+
+/// The execution readout data from a particular memory location.
+#[derive(Serialize)]
+pub struct ExecutionResult {
+    shape: Vec<usize>,
+    data: Register,
+    dtype: String,
+}
+
+/// Execution readout data for all memory locations.
+#[derive(Serialize)]
+pub struct ExecutionResults {
+    buffers: HashMap<String, ExecutionResult>,
+    execution_duration_microseconds: Option<u64>,
+}
+
+/// Fetches results for the corresponding job
+///
+/// # Errors
+///
+/// May error if an engagement is not available, an RPCQ client cannot be built,
+/// or retrieval of results fails.
+///
+/// # Panics
+///
+/// Panics if a [`Register`] cannot be constructed from a result buffer.
+pub async fn retrieve_results(
+    job_id: &str,
+    quantum_processor_id: &str,
+    config: &Configuration,
+) -> Result<ExecutionResults, runner::Error> {
+    let engagement = engagement::get(String::from(quantum_processor_id), config)
+        .await
+        .map_err(|e| runner::Error::Qpu(format!("Unable to get engagement: {:?}", e)))?;
+    let rpcq_client = Client::try_from(&engagement).map_err(|e| {
+        warn!("Unable to connect to QPU via RPCQ: {:?}", e);
+        runner::Error::Qpu(format!("Unable to connect to QPU via RPCQ: {:?}", e))
+    })?;
+
+    let results = runner::retrieve_results(JobId(job_id.to_string()), &rpcq_client)?;
+    let execution_duration_microseconds = results.execution_duration_microseconds;
+    let buffers = results
+        .buffers
+        .into_iter()
+        .map(|(name, buffer)| {
+            let shape = buffer.shape.clone();
+            let dtype = buffer.dtype.to_string();
+            // TODO Get rid of this unwrap.
+            let data = Register::from(qpu::runner::Register::try_from(buffer).unwrap());
+            (name, ExecutionResult { shape, data, dtype })
+        })
+        .collect::<HashMap<_, _>>();
+
+    Ok(ExecutionResults {
+        buffers,
+        execution_duration_microseconds,
+    })
+}
diff --git a/src/configuration/mod.rs b/crates/lib/src/configuration/mod.rs
similarity index 84%
rename from src/configuration/mod.rs
rename to crates/lib/src/configuration/mod.rs
index 2d632241a..50513e0e0 100644
--- a/src/configuration/mod.rs
+++ b/crates/lib/src/configuration/mod.rs
@@ -28,41 +28,59 @@ mod settings;
 
 /// All the config data that's parsed from config sources
 #[derive(Clone, Debug)]
-pub(crate) struct Configuration {
+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(crate) enum RefreshError {
+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(crate) enum LoadError {
+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 { env: String },
+    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,
     },
 }
@@ -74,7 +92,7 @@ impl Configuration {
     /// # Errors
     ///
     /// See [`LoadError`].
-    pub(crate) async fn load() -> Result<Self, LoadError> {
+    pub async fn load() -> Result<Self, LoadError> {
         let (settings, secrets) = try_join(settings::load(), secrets::load()).await?;
         Self::new(settings, secrets)
     }
@@ -84,7 +102,7 @@ impl Configuration {
     /// # Errors
     ///
     /// See [`RefreshError`].
-    pub(crate) async fn refresh(mut self) -> Result<Self, 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);
diff --git a/src/configuration/path.rs b/crates/lib/src/configuration/path.rs
similarity index 100%
rename from src/configuration/path.rs
rename to crates/lib/src/configuration/path.rs
diff --git a/src/configuration/secrets.rs b/crates/lib/src/configuration/secrets.rs
similarity index 92%
rename from src/configuration/secrets.rs
rename to crates/lib/src/configuration/secrets.rs
index eecbaa270..31690a2df 100644
--- a/src/configuration/secrets.rs
+++ b/crates/lib/src/configuration/secrets.rs
@@ -27,7 +27,7 @@ mod describe_load {
 
     use tempfile::NamedTempFile;
 
-    use super::*;
+    use super::{load, Credential, Secrets, SECRETS_PATH_VAR};
 
     #[tokio::test]
     async fn it_returns_default_if_missing_path() {
@@ -36,7 +36,7 @@ mod describe_load {
         let settings = load().await;
 
         std::env::remove_var(SECRETS_PATH_VAR);
-        assert!(settings.is_err())
+        assert!(settings.is_err());
     }
 
     #[tokio::test]
@@ -47,13 +47,14 @@ mod describe_load {
             .credentials
             .insert("test".to_string(), Credential::default());
         let secrets_string = toml::to_string(&secrets).expect("Could not serialize test settings");
-        file.write(secrets_string.as_bytes())
+        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)
+        assert_eq!(secrets, loaded);
     }
 }
 
diff --git a/src/configuration/settings.rs b/crates/lib/src/configuration/settings.rs
similarity index 91%
rename from src/configuration/settings.rs
rename to crates/lib/src/configuration/settings.rs
index 2a9af97c1..c7f9b8934 100644
--- a/src/configuration/settings.rs
+++ b/crates/lib/src/configuration/settings.rs
@@ -26,7 +26,9 @@ mod describe_load {
 
     use tempfile::NamedTempFile;
 
-    use super::*;
+    use crate::configuration::{settings::load, SETTINGS_PATH_VAR};
+
+    use super::Settings;
 
     #[tokio::test]
     async fn it_returns_default_if_missing_path() {
@@ -36,23 +38,26 @@ mod describe_load {
 
         std::env::remove_var(SETTINGS_PATH_VAR);
 
-        assert!(settings.is_err())
+        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 settings = Settings::default();
-        settings.default_profile_name = "THIS IS A TEST".to_string();
+        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");
-        file.write(settings_string.as_bytes())
+        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)
+        assert_eq!(settings, loaded);
     }
 }
 
diff --git a/src/executable.rs b/crates/lib/src/executable.rs
similarity index 96%
rename from src/executable.rs
rename to crates/lib/src/executable.rs
index ed5bb3a70..f081bc9a1 100644
--- a/src/executable.rs
+++ b/crates/lib/src/executable.rs
@@ -6,7 +6,7 @@ use std::sync::Arc;
 use std::time::Duration;
 
 use crate::configuration::{Configuration, LoadError, RefreshError};
-use crate::qpu::{ExecutionError, JobId};
+use crate::qpu::{engagement, ExecutionError, JobId};
 use crate::{qpu, qvm, ExecutionData};
 
 /// The builder interface for executing Quil programs on QVMs and QPUs.
@@ -451,6 +451,9 @@ pub enum Error {
     /// [`Executable::retrieve_results`] can invalidate the handle.
     #[error("The job handle was not valid")]
     InvalidJobHandle,
+    /// This error indicates that there was no current QPU engagement available for the user.
+    #[error("No engagement for QPU: {0}")]
+    NoEngagement(engagement::Error),
 }
 
 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
@@ -501,6 +504,7 @@ impl From<RefreshError> for Error {
 impl From<ExecutionError> for Error {
     fn from(err: ExecutionError) -> Self {
         match err {
+            ExecutionError::NoEngagement(e) => Self::NoEngagement(e),
             ExecutionError::QpuNotFound => Self::QpuNotFound,
             ExecutionError::QpuUnavailable(duration) => Self::QpuUnavailable(duration),
             ExecutionError::Unauthorized => Self::Authentication,
@@ -546,7 +550,9 @@ impl JobHandle<'_> {
 #[cfg(test)]
 #[cfg(feature = "manual-tests")]
 mod describe_qpu_for_id {
-    use super::*;
+    use std::sync::Arc;
+
+    use crate::{configuration::Configuration, qpu, Executable};
 
     #[tokio::test]
     async fn it_refreshes_auth_token() {
@@ -560,7 +566,7 @@ mod describe_qpu_for_id {
             panic!("Expected an error!");
         };
         let result_string = format!("{:?}", err);
-        assert!(result_string.contains("refresh token"))
+        assert!(result_string.contains("refresh_token"));
     }
 
     #[tokio::test]
@@ -572,7 +578,7 @@ mod describe_qpu_for_id {
             qpu::Execution::new(
                 "".into(),
                 shots,
-                "Aspen-9",
+                "Aspen-11",
                 exe.get_config().await.unwrap_or_default(),
                 exe.compile_with_quilc,
             )
@@ -582,7 +588,7 @@ mod describe_qpu_for_id {
         // Load config with no credentials to prevent creating a new Execution if it tries
         exe.config = Some(Arc::new(Configuration::default()));
 
-        assert!(exe.qpu_for_id("Aspen-9").await.is_ok());
+        assert!(exe.qpu_for_id("Aspen-11").await.is_ok());
     }
 
     #[tokio::test]
@@ -622,17 +628,19 @@ mod describe_qpu_for_id {
 
 #[cfg(test)]
 #[cfg(feature = "manual-tests")]
-mod describe_take_or_load_config {
-    use super::*;
+mod describe_get_config {
+    use std::sync::Arc;
+
+    use crate::{configuration::Configuration, Executable};
 
     #[tokio::test]
     async fn it_returns_cached_values() {
         let mut exe = Executable::from_quil("");
-        let mut config = Arc::new(Configuration::default());
+        let mut config = Configuration::default();
         config.quilc_url = String::from("test");
+        let config = Arc::new(config);
         exe.config = Some(config.clone());
-        let gotten = exe.take_or_load_config().await;
+        let gotten = exe.get_config().await.unwrap_or_default();
         assert_eq!(gotten.quilc_url, config.quilc_url);
-        assert!(matches!(exe.config, None));
     }
 }
diff --git a/src/execution_data.rs b/crates/lib/src/execution_data.rs
similarity index 100%
rename from src/execution_data.rs
rename to crates/lib/src/execution_data.rs
diff --git a/src/lib.rs b/crates/lib/src/lib.rs
similarity index 95%
rename from src/lib.rs
rename to crates/lib/src/lib.rs
index 97948e5f9..9c3aabab7 100644
--- a/src/lib.rs
+++ b/crates/lib/src/lib.rs
@@ -13,9 +13,10 @@ pub use executable::{Error, Executable, JobHandle, Service};
 pub use execution_data::ExecutionData;
 pub use register_data::RegisterData;
 
+pub mod api;
 pub mod configuration;
 mod executable;
 mod execution_data;
-mod qpu;
+pub mod qpu;
 mod qvm;
 mod register_data;
diff --git a/src/qpu/engagement.rs b/crates/lib/src/qpu/engagement.rs
similarity index 98%
rename from src/qpu/engagement.rs
rename to crates/lib/src/qpu/engagement.rs
index 6c6f671c7..a7092aa1d 100644
--- a/src/qpu/engagement.rs
+++ b/crates/lib/src/qpu/engagement.rs
@@ -8,7 +8,7 @@ use qcs_api::models::{CreateEngagementRequest, EngagementWithCredentials, Error
 use crate::configuration::Configuration;
 
 /// Try to get an engagement for a QPU
-pub(super) async fn get(
+pub(crate) async fn get(
     quantum_processor_id: String,
     config: &Configuration,
 ) -> Result<EngagementWithCredentials, Error> {
@@ -82,7 +82,7 @@ async fn create_engagement(
 const DEFAULT_RETRY_AFTER: Duration = Duration::from_secs(15 /* minutes */ * 60);
 
 #[derive(thiserror::Error, Debug)]
-pub(super) enum Error {
+pub enum Error {
     #[error("the QPU is unavailable, try again after {} seconds", .0.as_secs())]
     QuantumProcessorUnavailable(Duration),
     #[error("Received an unauthorized response, try refreshing the token")]
diff --git a/src/qpu/execution.rs b/crates/lib/src/qpu/execution.rs
similarity index 89%
rename from src/qpu/execution.rs
rename to crates/lib/src/qpu/execution.rs
index 16a319bc0..0dd43bf22 100644
--- a/src/qpu/execution.rs
+++ b/crates/lib/src/qpu/execution.rs
@@ -6,16 +6,16 @@ use std::sync::{Arc, Mutex};
 use std::time::Duration;
 
 use log::{trace, warn};
-use quil_rs::expression::Expression;
 use tokio::task::{spawn_blocking, JoinError};
 
 use crate::configuration::Configuration;
 use crate::executable::Parameters;
+use crate::qpu::rewrite_arithmetic;
 use crate::qpu::runner::JobId;
 use crate::{ExecutionData, RegisterData};
 
-use super::quilc::{self, NativeQuil, NativeQuilProgram};
-use super::rewrite_arithmetic::{RewrittenProgram, SUBSTITUTION_NAME};
+use super::quilc::{self, NativeQuil, NativeQuilProgram, TargetDevice};
+use super::rewrite_arithmetic::RewrittenProgram;
 use super::rpcq::Client;
 use super::runner::{self, retrieve_results, submit, DecodeError};
 use super::translation::Error as TranslationError;
@@ -37,6 +37,8 @@ pub(crate) struct Execution<'a> {
 
 #[derive(Debug, thiserror::Error)]
 pub(crate) enum Error {
+    #[error("No engagement for QPU: {0}")]
+    NoEngagement(engagement::Error),
     #[error("QPU not found")]
     QpuNotFound,
     #[error("QPU currently unavailable, retry after {} seconds", .0.as_secs())]
@@ -92,11 +94,14 @@ impl From<engagement::Error> for Error {
 impl From<runner::Error> for Error {
     fn from(source: runner::Error) -> Self {
         match source {
+            runner::Error::Engagement(e) => Self::NoEngagement(e),
             runner::Error::Connection(_) => Self::QcsCommunication,
+            //runner::Error::Engagement(_) => Self::
             runner::Error::Unexpected(err) => {
                 Self::Unexpected(Unexpected::Qcs(format!("{:?}", err)))
             }
             runner::Error::Qpu(err) => Self::Qcs(err),
+            runner::Error::ClientLock(err) => Self::Unexpected(Unexpected::Other(err)),
         }
     }
 }
@@ -113,6 +118,8 @@ pub(crate) enum Unexpected {
     Isa(String),
     #[error("Problem understanding QCS")]
     Qcs(String),
+    #[error("Unknown error")]
+    Other(String),
 }
 
 impl From<IsaError> for Error {
@@ -189,11 +196,12 @@ impl<'a> Execution<'a> {
         compile_with_quilc: bool,
     ) -> Result<Execution<'a>, Error> {
         let isa = get_isa(quantum_processor_id, &config).await?;
+        let target_device = TargetDevice::try_from(isa)?;
 
         let native_quil = if compile_with_quilc {
             trace!("Converting to Native Quil");
             let thread_config = config.clone();
-            spawn_blocking(move || quilc::compile_program(&quil, isa, &thread_config))
+            spawn_blocking(move || quilc::compile_program(&quil, target_device, &thread_config))
                 .await
                 .map_err(|source| {
                     Error::Unexpected(Unexpected::TaskError {
@@ -334,36 +342,6 @@ impl<'a> Execution<'a> {
     /// `__SUBST` by calculating the original expressions given the user-provided params (in this
     /// case just `theta`).
     fn get_substitutions(&self, params: &Parameters) -> Result<Parameters, String> {
-        // Convert into the format that quil-rs expects.
-        let params: HashMap<&str, Vec<f64>> = params
-            .iter()
-            .map(|(key, value)| (key.as_ref(), value.clone()))
-            .collect();
-        let values = self
-            .program
-            .substitutions
-            .iter()
-            .map(|substitution: &Expression| {
-                substitution
-                    .evaluate(&HashMap::new(), &params)
-                    .map_err(|_| format!("Could not evaluate expression {}", substitution))
-                    .and_then(|complex| {
-                        if complex.im == 0.0 {
-                            Ok(complex.re)
-                        } else {
-                            Err(String::from(
-                                "Cannot substitute imaginary numbers for QPU execution",
-                            ))
-                        }
-                    })
-            })
-            .collect::<Result<Vec<f64>, String>>()?;
-        // Convert back to the format that this library expects
-        let mut patch_values: Parameters = params
-            .into_iter()
-            .map(|(key, value)| (key.into(), value))
-            .collect();
-        patch_values.insert(SUBSTITUTION_NAME.into(), values);
-        Ok(patch_values)
+        rewrite_arithmetic::get_substitutions(&self.program.substitutions, params)
     }
 }
diff --git a/src/qpu/mod.rs b/crates/lib/src/qpu/mod.rs
similarity index 97%
rename from src/qpu/mod.rs
rename to crates/lib/src/qpu/mod.rs
index a28184002..c48d5e1e6 100644
--- a/src/qpu/mod.rs
+++ b/crates/lib/src/qpu/mod.rs
@@ -19,18 +19,18 @@ use runner::Buffer;
 use translation::{translate, Error as TranslationError};
 
 use crate::configuration::Configuration;
-use crate::qpu::rewrite_arithmetic::RewrittenQuil;
+pub(crate) use crate::qpu::rewrite_arithmetic::RewrittenQuil;
 pub(crate) use crate::qpu::runner::DecodeError;
 pub(crate) use crate::qpu::runner::JobId;
 pub(crate) use crate::qpu::runner::Register;
 
-mod engagement;
+pub(crate) mod engagement;
 mod execution;
-mod quilc;
-mod rewrite_arithmetic;
-mod rpcq;
-mod runner;
-mod translation;
+pub mod quilc;
+pub(crate) mod rewrite_arithmetic;
+pub(crate) mod rpcq;
+pub(crate) mod runner;
+pub(crate) mod translation;
 
 async fn build_executable(
     quil: RewrittenQuil,
@@ -46,7 +46,7 @@ async fn build_executable(
 /// Process the buffers that come back from a QPU call and map them to the
 /// `buffer_names` provided by the translation service, then attempt to fit that data into the (generic)
 /// requested structure.
-fn process_buffers(
+pub(crate) fn process_buffers(
     mut buffers: HashMap<String, Buffer>,
     buffer_names: &HashMap<Box<str>, Vec<String>>,
 ) -> Result<HashMap<Box<str>, Vec<Register>>, DecodeError> {
@@ -221,7 +221,7 @@ fn organize_ro_sources(
 mod describe_organize_ro_sources {
     use maplit::hashmap;
 
-    use super::*;
+    use crate::qpu::organize_ro_sources;
 
     #[test]
     fn it_converts_from_translation_ro_sources() {
@@ -287,7 +287,7 @@ mod describe_organize_ro_sources {
 /// 1. Problem communicating with QCS
 /// 2. Unauthenticated
 /// 3. Expired token
-async fn get_isa(
+pub(crate) async fn get_isa(
     quantum_processor_id: &str,
     config: &Configuration,
 ) -> Result<InstructionSetArchitecture, IsaError> {
diff --git a/src/qpu/quilc/isa/edge.rs b/crates/lib/src/qpu/quilc/isa/edge.rs
similarity index 69%
rename from src/qpu/quilc/isa/edge.rs
rename to crates/lib/src/qpu/quilc/isa/edge.rs
index 783a33e84..2fa88b009 100644
--- a/src/qpu/quilc/isa/edge.rs
+++ b/crates/lib/src/qpu/quilc/isa/edge.rs
@@ -2,23 +2,22 @@ use std::collections::HashMap;
 use std::convert::{TryFrom, TryInto};
 use std::fmt::{Display, Formatter};
 
-use serde::{Serialize, Serializer};
+use serde::{Deserialize, Serialize, Serializer};
 
 use qcs_api::models;
 use qcs_api::models::Characteristic;
 
-use super::operator::{
-    Arguments, Operator, OperatorMap, Parameters, PERFECT_DURATION, PERFECT_FIDELITY,
-};
+use super::operator::{wildcard, Argument, Operator, Parameter};
 
 /// Represents a connection between two qubits.
-#[derive(Serialize, Debug, Clone, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
 pub(crate) struct Edge {
     #[serde(rename = "ids")]
-    id: EdgeId,
+    id: Id,
     #[serde(skip_serializing_if = "is_false")]
+    #[serde(default)]
     dead: bool,
-    gates: OperatorMap,
+    gates: Vec<Operator>,
 }
 
 // Gross hack to not include `dead` if unneeded, to follow pyQuil's implementation
@@ -29,11 +28,11 @@ fn is_false(val: &bool) -> bool {
 
 impl Edge {
     /// Construct an edge with no gates
-    fn empty(id: EdgeId) -> Self {
+    fn empty(id: Id) -> Self {
         Self {
             id,
             dead: true,
-            gates: OperatorMap::new(),
+            gates: vec![],
         }
     }
 
@@ -43,26 +42,28 @@ impl Edge {
         characteristics: &[Characteristic],
     ) -> Result<(), Error> {
         let operator = match GATE_PARAMS.get_key_value(op_name) {
-            Some((key, params)) => basic_gates(key, params, characteristics),
+            Some((key, params)) => basic_gates(key.clone(), params, characteristics),
             _ => {
                 if op_name == "WILDCARD" {
-                    WILDCARD
+                    wildcard(None)
                 } else {
                     return Err(Error::UnknownOperator(String::from(op_name)));
                 }
             }
         };
 
-        if self.gates.add_one(operator) {
-            self.dead = false;
-        }
+        // If an edge has an operation, it's not dead.
+        self.dead = false;
+
+        self.gates.push(operator);
+
         Ok(())
     }
 }
 
 /// All the error which can occur from within this module.
 #[derive(Debug, thiserror::Error)]
-pub(crate) enum Error {
+pub enum Error {
     #[error("Unknown operator: {0}")]
     UnknownOperator(String),
     #[error("Edges should have exactly 2 nodes, got {0}")]
@@ -71,19 +72,19 @@ pub(crate) enum Error {
 
 #[cfg(test)]
 mod describe_edge {
-    use super::*;
+    use crate::qpu::quilc::isa::edge::{Edge, Id};
 
     #[test]
     fn it_skips_serializing_dead_if_false() {
         let undead_qubit = Edge {
-            id: EdgeId::new([1, 2]),
+            id: Id::new([1, 2]),
             dead: false,
-            gates: OperatorMap::new(),
+            gates: vec![],
         };
         let dead_qubit = Edge {
-            id: EdgeId::new([1, 2]),
+            id: Id::new([1, 2]),
             dead: true,
-            gates: OperatorMap::new(),
+            gates: vec![],
         };
 
         let expected_dead = serde_json::json!({
@@ -109,17 +110,17 @@ mod describe_edge {
 /// key should look like `"{node_id_1}-{node_id_2}"`
 ///
 /// This struct enforces those things to make looking up of Edges easier when converting ISAs.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub(crate) struct EdgeId([i32; 2]);
+#[derive(Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Id([i32; 2]);
 
-impl EdgeId {
-    pub(crate) fn new(mut node_ids: [i32; 2]) -> Self {
+impl Id {
+    pub fn new(mut node_ids: [i32; 2]) -> Self {
         node_ids.sort_unstable();
         Self(node_ids)
     }
 }
 
-impl TryFrom<&Vec<i32>> for EdgeId {
+impl TryFrom<&Vec<i32>> for Id {
     type Error = Error;
 
     fn try_from(node_ids: &Vec<i32>) -> Result<Self, Error> {
@@ -131,7 +132,7 @@ impl TryFrom<&Vec<i32>> for EdgeId {
     }
 }
 
-impl Serialize for EdgeId {
+impl Serialize for Id {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
@@ -140,7 +141,7 @@ impl Serialize for EdgeId {
     }
 }
 
-impl Display for EdgeId {
+impl Display for Id {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}-{}", self.0[0], self.0[1])
     }
@@ -148,24 +149,26 @@ impl Display for EdgeId {
 
 #[cfg(test)]
 mod describe_edge_id {
-    use super::*;
+    use std::convert::TryFrom;
+
+    use crate::qpu::quilc::isa::edge::Id;
 
     #[test]
     fn it_serializes_as_an_int_list() {
-        let edge_id = EdgeId::new([1, 2]);
-        let serialized = serde_json::to_value(&edge_id).expect("Could not serialize EdgeId");
+        let edge_id = Id::new([1, 2]);
+        let serialized = serde_json::to_value(edge_id).expect("Could not serialize EdgeId");
         assert_eq!(serialized, serde_json::json!([1, 2]));
     }
 
     #[test]
     fn it_displays_as_quilc_style_string() {
-        let edge_id = EdgeId::new([1, 2]);
+        let edge_id = Id::new([1, 2]);
         assert_eq!(edge_id.to_string(), "1-2".to_string());
     }
 
     #[test]
     fn it_sorts_ids_when_constructed() {
-        let edge_id = EdgeId::new([2, 1]);
+        let edge_id = Id::new([2, 1]);
         assert_eq!(edge_id.to_string(), "1-2".to_string());
     }
 
@@ -180,7 +183,7 @@ mod describe_edge_id {
             vec![1, 2, 3],
         ];
         for input in inputs {
-            let result = EdgeId::try_from(&input);
+            let result = Id::try_from(&input);
             assert!(result.is_err());
         }
     }
@@ -188,17 +191,17 @@ mod describe_edge_id {
     #[test]
     fn it_successfully_converts_from_correct_vec() {
         let input = vec![2, 1];
-        let result = EdgeId::try_from(&input).expect("Failed to convert valid Vec to EdgeId");
+        let result = Id::try_from(&input).expect("Failed to convert valid Vec to EdgeId");
         assert_eq!(result.to_string(), "1-2".to_string());
     }
 }
 
 /// Convert the QCS ISA representation of edges to the quilc form [`Edge`]
-pub(crate) fn convert_edges(edges: &[models::Edge]) -> Result<HashMap<EdgeId, Edge>, Error> {
+pub(crate) fn convert_edges(edges: &[models::Edge]) -> Result<HashMap<Id, Edge>, Error> {
     edges
         .iter()
         .map(|edge| {
-            let id = EdgeId::try_from(&edge.node_ids)?;
+            let id = Id::try_from(&edge.node_ids)?;
             let edge = Edge::empty(id);
             Ok((id, edge))
         })
@@ -207,7 +210,9 @@ pub(crate) fn convert_edges(edges: &[models::Edge]) -> Result<HashMap<EdgeId, Ed
 
 #[cfg(test)]
 mod describe_convert_edges {
-    use super::*;
+    use qcs_api::models;
+
+    use crate::qpu::quilc::isa::edge::{convert_edges, Id};
 
     #[test]
     fn it_converts_valid_edges() {
@@ -226,43 +231,39 @@ mod describe_convert_edges {
         let result = convert_edges(&input).expect("Could not convert valid inputs");
 
         assert_eq!(result.len(), 3);
-        let expected_ids = [
-            EdgeId::new([1, 2]),
-            EdgeId::new([2, 3]),
-            EdgeId::new([1, 3]),
-        ];
+        let expected_ids = [Id::new([1, 2]), Id::new([2, 3]), Id::new([1, 3])];
         for expected in expected_ids {
-            assert_eq!(result[&expected].id, expected)
+            assert_eq!(result[&expected].id, expected);
         }
     }
 }
 
 lazy_static::lazy_static! {
-    static ref GATE_PARAMS: HashMap<&'static str, GateParams> = {
+    static ref GATE_PARAMS: HashMap<String, GateParams> = {
         let mut m = HashMap::new();
-        m.insert("CZ", GateParams{
+        m.insert("CZ".to_string(), GateParams{
             default_fidelity: 0.89,
             duration: 200.0,
-            characteristic_name: "fCZ",
-            parameters: Parameters::Empty,
+            characteristic_name: "fCZ".to_string(),
+            parameters: vec![],
         });
-        m.insert("ISWAP", GateParams{
+        m.insert("ISWAP".to_string(), GateParams{
             default_fidelity: 0.90,
             duration: 200.0,
-            characteristic_name: "fISWAP",
-            parameters: Parameters::Empty,
+            characteristic_name: "fISWAP".to_string(),
+            parameters: vec![],
         });
-        m.insert("CPHASE", GateParams{
+        m.insert("CPHASE".to_string(), GateParams{
             default_fidelity: 0.85,
             duration: 200.0,
-            characteristic_name: "fCPHASE",
-            parameters: Parameters::Theta,
+            characteristic_name: "fCPHASE".to_string(),
+            parameters: vec![Parameter::String("theta".to_owned())],
         });
-        m.insert("XY", GateParams{
+        m.insert("XY".to_string(), GateParams{
             default_fidelity: 0.86,
             duration: 200.0,
-            characteristic_name: "fXY",
-            parameters: Parameters::Theta,
+            characteristic_name: "fXY".to_string(),
+            parameters: vec![Parameter::String("theta".to_owned())],
         });
         m
     };
@@ -272,12 +273,12 @@ lazy_static::lazy_static! {
 struct GateParams {
     default_fidelity: f64,
     duration: f64,
-    characteristic_name: &'static str,
-    parameters: Parameters,
+    characteristic_name: String,
+    parameters: Vec<Parameter>,
 }
 
 fn basic_gates(
-    op_name: &'static str,
+    op_name: String,
     params: &GateParams,
     characteristics: &[Characteristic],
 ) -> Operator {
@@ -296,17 +297,9 @@ fn basic_gates(
 
     Operator::Gate {
         operator: op_name,
-        parameters: *parameters,
-        arguments: Arguments::Underscores,
+        parameters: parameters.clone(),
+        arguments: vec![Argument::String("_".to_string()); 2],
         fidelity,
         duration: *duration,
     }
 }
-
-const WILDCARD: Operator = Operator::Gate {
-    operator: "_",
-    duration: PERFECT_DURATION,
-    fidelity: PERFECT_FIDELITY,
-    parameters: Parameters::Underscore,
-    arguments: Arguments::Underscores,
-};
diff --git a/src/qpu/quilc/isa/mod.rs b/crates/lib/src/qpu/quilc/isa/mod.rs
similarity index 83%
rename from src/qpu/quilc/isa/mod.rs
rename to crates/lib/src/qpu/quilc/isa/mod.rs
index 0c3a31135..4d35276bd 100644
--- a/src/qpu/quilc/isa/mod.rs
+++ b/crates/lib/src/qpu/quilc/isa/mod.rs
@@ -1,10 +1,9 @@
 use std::collections::HashMap;
 use std::convert::TryFrom;
 
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
-use edge::{convert_edges, Edge, EdgeId};
-use operator::Operator;
+use edge::{convert_edges, Edge, Id};
 use qcs_api::models::InstructionSetArchitecture;
 use qubit::Qubit;
 
@@ -15,15 +14,15 @@ mod operator;
 mod qubit;
 
 /// Restructuring of an [`InstructionSetArchitecture`] for sending to quilc
-#[derive(Serialize, Debug, Clone, PartialEq)]
-pub(crate) struct CompilerIsa {
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
+pub struct Compiler {
     #[serde(rename = "1Q")]
     qubits: HashMap<String, Qubit>,
     #[serde(rename = "2Q")]
     edges: HashMap<String, Edge>,
 }
 
-impl TryFrom<InstructionSetArchitecture> for CompilerIsa {
+impl TryFrom<InstructionSetArchitecture> for Compiler {
     type Error = Error;
 
     fn try_from(isa: InstructionSetArchitecture) -> Result<Self, Error> {
@@ -48,7 +47,7 @@ impl TryFrom<InstructionSetArchitecture> for CompilerIsa {
                     qubit.add_operation(&op.name, &site.characteristics, &frb_sim_1q)?;
                 }
                 (Some(2), 2) => {
-                    let id = EdgeId::try_from(&site.node_ids)?;
+                    let id = Id::try_from(&site.node_ids)?;
                     let edge = edges
                         .get_mut(&id)
                         .ok_or_else(|| Error::EdgeDoesNotExist(String::from(&op.name), id))?;
@@ -75,11 +74,11 @@ impl TryFrom<InstructionSetArchitecture> for CompilerIsa {
 
 /// All the errors that can occur from within this module
 #[derive(Debug, thiserror::Error)]
-pub(crate) enum Error {
+pub enum Error {
     #[error("Operation {0} is defined for Qubit {1} but that Qubit does not exist")]
     QubitDoesNotExist(String, i32),
     #[error("Operation {0} is defined for Edge {1} but that Edge does not exist")]
-    EdgeDoesNotExist(String, EdgeId),
+    EdgeDoesNotExist(String, Id),
     #[error(
         "The number of nodes for an operation and site_operation must be (1, 1) or (2, 2). \
                         Got {0:?} while parsing operation {1} at site {2:?}"
@@ -93,12 +92,13 @@ pub(crate) enum Error {
 
 #[cfg(test)]
 mod describe_compiler_isa {
-    use std::fs::read_to_string;
+    use std::{convert::TryFrom, fs::read_to_string};
 
     use float_cmp::{approx_eq, F64Margin};
+    use qcs_api::models::InstructionSetArchitecture;
     use serde_json::Value;
 
-    use super::*;
+    use super::Compiler;
 
     /// Compare two JSON values and make sure they are equivalent while allowing for some precision
     /// loss in numbers.
@@ -135,33 +135,32 @@ mod describe_compiler_isa {
                         break;
                     }
                     let cmp = json_is_equivalent(first_value, second_value.unwrap());
-                    if !cmp.is_ok() {
-                        return cmp;
-                    }
+                    cmp?;
                 }
                 !found_missing
             }
+            (Value::Array(first_array), Value::Array(second_array))
+                if first_array.len() != second_array.len() =>
+            {
+                false
+            }
             (Value::Array(first_array), Value::Array(second_array)) => {
-                if first_array.len() != second_array.len() {
-                    false
-                } else {
-                    let error = first_array.into_iter().zip(second_array).find(
-                        |(first_value, second_value)| {
-                            json_is_equivalent(first_value, second_value).is_err()
-                        },
-                    );
-                    if let Some(values) = error {
-                        return Err(values);
-                    }
-                    true
+                let error = first_array.iter().zip(second_array).find(
+                    |(first_value, second_value)| -> bool {
+                        json_is_equivalent(first_value, second_value).is_err()
+                    },
+                );
+                if let Some(values) = error {
+                    return Err(values);
                 }
+                true
             }
             (first, second) => first == second,
         };
-        if !equal {
-            Err((first, second))
-        } else {
+        if equal {
             Ok(())
+        } else {
+            Err((first, second))
         }
     }
 
@@ -177,7 +176,7 @@ mod describe_compiler_isa {
             serde_json::from_str(&expected_json).expect("Could not deserialize Aspen-8 output");
 
         let compiler_isa =
-            CompilerIsa::try_from(qcs_isa).expect("Could not convert ISA to CompilerIsa");
+            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");
 
diff --git a/crates/lib/src/qpu/quilc/isa/operator.rs b/crates/lib/src/qpu/quilc/isa/operator.rs
new file mode 100644
index 000000000..47b3e330a
--- /dev/null
+++ b/crates/lib/src/qpu/quilc/isa/operator.rs
@@ -0,0 +1,180 @@
+use serde::{Deserialize, Serialize};
+
+pub(crate) fn wildcard(node_id: Option<i32>) -> Operator {
+    let arg = node_id.map_or_else(|| Argument::String("_".to_owned()), Argument::Int);
+    Operator::Gate {
+        operator: "_".to_string(),
+        duration: PERFECT_DURATION,
+        fidelity: PERFECT_FIDELITY,
+        parameters: vec![Parameter::String("_".to_owned())],
+        arguments: vec![arg],
+    }
+}
+
+/// Represents a single operation that can be performed on a Qubit or Edge
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
+#[serde(tag = "operator_type")]
+#[serde(rename_all = "lowercase")]
+pub(crate) enum Operator {
+    Gate {
+        operator: String,
+        duration: f64,
+        fidelity: f64,
+        parameters: Vec<Parameter>,
+        arguments: Vec<Argument>,
+    },
+    Measure {
+        operator: String,
+        duration: f64,
+        fidelity: f64,
+        qubit: i32,
+        target: Option<String>,
+    },
+}
+
+#[cfg(test)]
+mod describe_operator {
+    use crate::qpu::quilc::isa::operator::{Argument, Operator, Parameter};
+
+    /// This test copies some JSON data from the pyQuil ISA integration test to
+    /// validate that [`Operator::Gate`] is serialized correctly.
+    #[test]
+    fn it_serializes_gates_like_pyquil() {
+        let gate_op = Operator::Gate {
+            arguments: vec![Argument::Int(1)],
+            duration: 0.5,
+            fidelity: 0.5,
+            operator: "RZ".to_string(),
+            parameters: vec![Parameter::String("_".to_owned())],
+        };
+        let expected = serde_json::json!({
+            "arguments": [1],
+            "duration": 0.5,
+            "fidelity": 0.5,
+            "operator": "RZ",
+            "operator_type": "gate",
+            "parameters": ["_"]
+        });
+        let serialized =
+            serde_json::to_value(&gate_op).expect("Could not serialize Operation::Gate");
+        assert_eq!(serialized, expected);
+    }
+
+    /// This test copies some JSON data from the pyQuil ISA integration test to validate that
+    /// [`Operator::Measure`] is serialized correctly.
+    #[test]
+    fn it_serializes_measurements_like_pyquil() {
+        let measure = Operator::Measure {
+            duration: 0.5,
+            fidelity: 0.5,
+            qubit: 1,
+            operator: "MEASURE".to_string(),
+            target: Some("_".to_string()),
+        };
+        let expected = serde_json::json!({
+            "duration": 0.5,
+            "fidelity": 0.5,
+            "operator": "MEASURE",
+            "operator_type": "measure",
+            "qubit": 1,
+            "target": "_"
+        });
+        let serialized =
+            serde_json::to_value(&measure).expect("Could not serialize Operation::Gate");
+        assert_eq!(serialized, expected);
+    }
+
+    /// This test copies some JSON data from the pyQuil ISA integration test to validate that
+    /// [`Operator::Measure`] is serialized correctly.
+    #[test]
+    fn it_serializes_measurements_with_null_targets_like_pyquil() {
+        let measure = Operator::Measure {
+            duration: 0.5,
+            fidelity: 0.5,
+            qubit: 1,
+            operator: "MEASURE".to_string(),
+            target: None,
+        };
+        let expected = serde_json::json!({
+            "duration": 0.5,
+            "fidelity": 0.5,
+            "operator": "MEASURE",
+            "operator_type": "measure",
+            "qubit": 1,
+            "target": null
+        });
+        let serialized =
+            serde_json::to_value(&measure).expect("Could not serialize Operation::Gate");
+        assert_eq!(serialized, expected);
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
+#[serde(untagged)]
+pub(crate) enum Parameter {
+    String(String),
+    Float(f64),
+}
+
+#[cfg(test)]
+mod test_parameters_deser {
+    use super::Parameter;
+
+    #[test]
+    fn it_deserializes_parameters() {
+        let s = r##"[""]"##;
+        serde_json::from_str::<Vec<Parameter>>(s).expect("yo");
+    }
+}
+
+#[cfg(test)]
+mod describe_parameters {
+    use crate::qpu::quilc::isa::operator::Parameter;
+
+    #[test]
+    fn it_serializes_underscore_as_list_of_strings() {
+        let p = Parameter::String("_".to_owned());
+        let serialized = serde_json::to_value(p).expect("Could not serialize");
+        let expected = serde_json::json!("_");
+        assert_eq!(expected, serialized);
+    }
+
+    #[test]
+    fn it_serializes_one_float_as_list_of_numbers() {
+        let p = Parameter::Float(1.0);
+        let serialized = serde_json::to_value(p).expect("Could not serialize");
+        let expected = serde_json::json!(1.0);
+        assert_eq!(expected, serialized);
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+#[serde(untagged)]
+pub(crate) enum Argument {
+    Int(i32),
+    String(String),
+}
+
+#[cfg(test)]
+mod describe_arguments {
+    use crate::qpu::quilc::isa::operator::Argument;
+
+    #[test]
+    fn it_serializes_underscores_as_list_of_strings() {
+        let p = vec![Argument::String("_".to_owned())];
+        let serialized = serde_json::to_value(p).expect("Could not serialize");
+        let expected = serde_json::json!(["_"]);
+        assert_eq!(expected, serialized);
+    }
+
+    #[test]
+    fn it_serializes_one_int_as_list_of_numbers() {
+        let p = vec![Argument::Int(1)];
+        let serialized = serde_json::to_value(p).expect("Could not serialize");
+        let expected = serde_json::json!([1]);
+        assert_eq!(expected, serialized);
+    }
+}
+
+pub(crate) const PERFECT_FIDELITY: f64 = 1.0;
+pub(crate) const PERFECT_DURATION: f64 = 1.0 / 100.0;
diff --git a/src/qpu/quilc/isa/qubit.rs b/crates/lib/src/qpu/quilc/isa/qubit.rs
similarity index 76%
rename from src/qpu/quilc/isa/qubit.rs
rename to crates/lib/src/qpu/quilc/isa/qubit.rs
index 46bec8fa0..c54afffef 100644
--- a/src/qpu/quilc/isa/qubit.rs
+++ b/crates/lib/src/qpu/quilc/isa/qubit.rs
@@ -2,20 +2,22 @@ use std::collections::HashMap;
 use std::convert::TryFrom;
 use std::f64::consts::{FRAC_PI_2, PI};
 
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 
 use qcs_api::models::{Characteristic, Node, Operation};
 
-use super::operator::{Arguments, OperatorMap, Parameters, PERFECT_DURATION, PERFECT_FIDELITY};
-use super::Operator;
+use super::operator::{
+    wildcard, Argument, Operator, Parameter, PERFECT_DURATION, PERFECT_FIDELITY,
+};
 
 /// Represents a single Qubit on a QPU and its capabilities. Needed by quilc for optimization.
-#[derive(Serialize, Debug, Clone, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
 pub(crate) struct Qubit {
     id: i32,
     #[serde(skip_serializing_if = "is_false")]
+    #[serde(default)]
     dead: bool,
-    gates: OperatorMap,
+    gates: Vec<Operator>,
 }
 
 // Gross hack to not include `dead` if unneeded, to follow pyQuil's implementation
@@ -62,20 +64,19 @@ impl Qubit {
             "RX" => rx_gates(self.id, frb_sim_1q)?,
             "RZ" => rz_gates(self.id),
             "MEASURE" => measure(self.id, characteristics),
-            "WILDCARD" => wildcard(self.id),
+            "WILDCARD" => vec![wildcard(Some(self.id))],
             "I" | "RESET" => vec![],
             unknown => return Err(Error::UnknownOperator(String::from(unknown))),
         };
-        if self.gates.add(&operators) {
-            self.dead = false;
-        }
+        self.gates.extend(operators);
+        self.dead = false;
         Ok(())
     }
 }
 
 /// All the errors that can occur within this module.
 #[derive(Debug, thiserror::Error)]
-pub(crate) enum Error {
+pub enum Error {
     #[error("Required benchmark `randomized_benchmark_simultaneous_1q` was not present")]
     MissingBenchmark,
     #[error("Benchmark `randomized_benchmark_simultaneous_1q` must have a single call site")]
@@ -91,26 +92,26 @@ impl From<&Node> for Qubit {
         Self {
             id: node.node_id,
             dead: true,
-            gates: OperatorMap::new(),
+            gates: vec![],
         }
     }
 }
 
 #[cfg(test)]
 mod describe_qubit {
-    use super::*;
+    use crate::qpu::quilc::isa::qubit::Qubit;
 
     #[test]
     fn it_skips_serializing_dead_if_false() {
         let undead_qubit = Qubit {
             id: 0,
             dead: false,
-            gates: OperatorMap::new(),
+            gates: vec![],
         };
         let dead_qubit = Qubit {
             id: 0,
             dead: true,
-            gates: OperatorMap::new(),
+            gates: vec![],
         };
 
         let expected_dead = serde_json::json!({
@@ -172,20 +173,20 @@ fn rx_gates(node_id: i32, frb_sim_1q: &FrbSim1q) -> Result<Vec<Operator>, Error>
     let fidelity = frb_sim_1q.fidelity_for_qubit(node_id)?;
 
     let mut gates = Vec::with_capacity(5);
-    let operator = "RX";
+    let operator = "RX".to_string();
     gates.push(Operator::Gate {
-        operator,
-        parameters: Parameters::Float(0.0),
-        arguments: Arguments::Int(node_id),
+        operator: operator.clone(),
+        parameters: vec![Parameter::Float(0.0)],
+        arguments: vec![Argument::Int(node_id)],
         fidelity: 1.0,
         duration: DEFAULT_DURATION_RX,
     });
 
     gates.extend(
         IntoIterator::into_iter([PI, -PI, FRAC_PI_2, -FRAC_PI_2]).map(|param| Operator::Gate {
-            operator,
-            parameters: Parameters::Float(param),
-            arguments: Arguments::Int(node_id),
+            operator: operator.clone(),
+            parameters: vec![Parameter::Float(param)],
+            arguments: vec![Argument::Int(node_id)],
             fidelity,
             duration: DEFAULT_DURATION_RX,
         }),
@@ -195,7 +196,14 @@ fn rx_gates(node_id: i32, frb_sim_1q: &FrbSim1q) -> Result<Vec<Operator>, Error>
 
 #[cfg(test)]
 mod describe_rx_gates {
-    use super::*;
+    use std::f64::consts::{FRAC_PI_2, PI};
+
+    use qcs_api::models::Characteristic;
+
+    use crate::qpu::quilc::isa::{
+        operator::{Argument, Operator, Parameter},
+        qubit::{rx_gates, FrbSim1q},
+    };
 
     /// This data is copied from the pyQuil ISA integration test.
     #[test]
@@ -204,16 +212,16 @@ mod describe_rx_gates {
         let frb_sim_1q = FrbSim1q(vec![
             Characteristic {
                 name: "fRB".to_string(),
-                value: 0.989_821_537_688_075,
-                error: Some(0.000_699_235_456_806_402),
+                value: 0.989_821_55,
+                error: Some(0.000_699_235_5),
                 node_ids: Some(vec![0]),
                 parameter_values: None,
                 timestamp: "1970-01-01T00:00:00+00:00".to_string(),
             },
             Characteristic {
                 name: "fRB".to_string(),
-                value: 0.996_832_638_579_018,
-                error: Some(0.000_100_896_782_153_99),
+                value: 0.996_832_6,
+                error: Some(0.000_100_896_78),
                 node_ids: Some(vec![1]),
                 timestamp: "1970-01-01T00:00:00+00:00".to_string(),
                 parameter_values: None,
@@ -222,39 +230,39 @@ mod describe_rx_gates {
         let gates = rx_gates(node_id, &frb_sim_1q).expect("Failed to create RX gates");
         let expected = vec![
             Operator::Gate {
-                arguments: Arguments::Int(1),
+                arguments: vec![Argument::Int(1)],
                 duration: 50.0,
                 fidelity: 1.0,
-                operator: "RX",
-                parameters: Parameters::Float(0.0),
+                operator: "RX".to_string(),
+                parameters: vec![Parameter::Float(0.0)],
             },
             Operator::Gate {
-                arguments: Arguments::Int(1),
+                arguments: vec![Argument::Int(1)],
                 duration: 50.0,
                 fidelity: 0.996_832_609_176_635_7,
-                operator: "RX",
-                parameters: Parameters::Float(PI),
+                operator: "RX".to_string(),
+                parameters: vec![Parameter::Float(PI)],
             },
             Operator::Gate {
-                arguments: Arguments::Int(1),
+                arguments: vec![Argument::Int(1)],
                 duration: 50.0,
                 fidelity: 0.996_832_609_176_635_7,
-                operator: "RX",
-                parameters: Parameters::Float(-PI),
+                operator: "RX".to_string(),
+                parameters: vec![Parameter::Float(-PI)],
             },
             Operator::Gate {
-                arguments: Arguments::Int(1),
+                arguments: vec![Argument::Int(1)],
                 duration: 50.0,
                 fidelity: 0.996_832_609_176_635_7,
-                operator: "RX",
-                parameters: Parameters::Float(FRAC_PI_2),
+                operator: "RX".to_string(),
+                parameters: vec![Parameter::Float(FRAC_PI_2)],
             },
             Operator::Gate {
-                arguments: Arguments::Int(1),
+                arguments: vec![Argument::Int(1)],
                 duration: 50.0,
                 fidelity: 0.996_832_609_176_635_7,
-                operator: "RX",
-                parameters: Parameters::Float(-FRAC_PI_2),
+                operator: "RX".to_string(),
+                parameters: vec![Parameter::Float(-FRAC_PI_2)],
             },
         ];
         assert_eq!(gates, expected);
@@ -263,17 +271,17 @@ mod describe_rx_gates {
 
 fn rz_gates(node_id: i32) -> Vec<Operator> {
     vec![Operator::Gate {
-        operator: "RZ",
-        parameters: Parameters::Underscore,
+        operator: "RZ".to_string(),
+        parameters: vec![Parameter::String("_".to_owned())],
         fidelity: PERFECT_FIDELITY,
         duration: PERFECT_DURATION,
-        arguments: Arguments::Int(node_id),
+        arguments: vec![Argument::Int(node_id)],
     }]
 }
 
 #[cfg(test)]
 mod describe_rz_gates {
-    use super::{rz_gates, Arguments, Operator, Parameters};
+    use super::{rz_gates, Argument, Operator, Parameter};
 
     /// This data is copied from the pyQuil ISA integration test.
     #[test]
@@ -281,11 +289,11 @@ mod describe_rz_gates {
         let node_id = 1;
         let gates = rz_gates(node_id);
         let expected = vec![Operator::Gate {
-            arguments: Arguments::Int(1),
+            arguments: vec![Argument::Int(1)],
             duration: 0.01,
             fidelity: 1.0,
-            operator: "RZ",
-            parameters: Parameters::Underscore,
+            operator: "RZ".to_string(),
+            parameters: vec![Parameter::String("_".to_owned())],
         }];
         assert_eq!(gates, expected);
     }
@@ -305,14 +313,14 @@ fn measure(node_id: i32, characteristics: &[Characteristic]) -> Vec<Operator> {
 
     vec![
         Operator::Measure {
-            operator: "MEASURE",
+            operator: "MEASURE".to_string(),
             duration: MEASURE_DEFAULT_DURATION,
             fidelity,
             qubit: node_id,
-            target: Some("_"),
+            target: Some("_".to_string()),
         },
         Operator::Measure {
-            operator: "MEASURE",
+            operator: "MEASURE".to_string(),
             duration: MEASURE_DEFAULT_DURATION,
             fidelity,
             qubit: node_id,
@@ -323,7 +331,11 @@ fn measure(node_id: i32, characteristics: &[Characteristic]) -> Vec<Operator> {
 
 #[cfg(test)]
 mod describe_measure {
-    use super::*;
+    use qcs_api::models::Characteristic;
+
+    use crate::qpu::quilc::isa::operator::Operator;
+
+    use super::measure;
 
     /// This test copies data from pyQuil's integration test for ISA conversion.
     #[test]
@@ -339,30 +351,20 @@ mod describe_measure {
         let result = measure(0, &characteristics);
         let expected = vec![
             Operator::Measure {
-                operator: "MEASURE",
+                operator: "MEASURE".to_string(),
                 duration: 2000.0,
                 fidelity: 0.981_000_006_198_883_1,
                 qubit: 0,
-                target: Some("_"),
+                target: Some("_".to_string()),
             },
             Operator::Measure {
-                operator: "MEASURE",
+                operator: "MEASURE".to_string(),
                 duration: 2000.0,
                 fidelity: 0.981_000_006_198_883_1,
                 qubit: 0,
                 target: None,
             },
         ];
-        assert_eq!(result, expected)
+        assert_eq!(result, expected);
     }
 }
-
-fn wildcard(node_id: i32) -> Vec<Operator> {
-    vec![Operator::Gate {
-        operator: "_",
-        duration: PERFECT_DURATION,
-        fidelity: PERFECT_FIDELITY,
-        parameters: Parameters::Underscore,
-        arguments: Arguments::Int(node_id),
-    }]
-}
diff --git a/src/qpu/quilc/mod.rs b/crates/lib/src/qpu/quilc/mod.rs
similarity index 81%
rename from src/qpu/quilc/mod.rs
rename to crates/lib/src/qpu/quilc/mod.rs
index 561f36ef4..955dd7e4b 100644
--- a/src/qpu/quilc/mod.rs
+++ b/crates/lib/src/qpu/quilc/mod.rs
@@ -1,3 +1,6 @@
+//! This module provides the functions and types necessary to compile a program
+//! using quilc.
+
 use std::collections::HashMap;
 use std::convert::TryFrom;
 use std::str::FromStr;
@@ -5,7 +8,7 @@ use std::str::FromStr;
 use quil_rs::Program;
 use serde::{Deserialize, Serialize};
 
-use isa::CompilerIsa;
+use isa::Compiler;
 use qcs_api::models::InstructionSetArchitecture;
 
 use crate::configuration::Configuration;
@@ -32,11 +35,11 @@ mod isa;
 /// be converted by `quilc`.
 pub(crate) fn compile_program(
     quil: &str,
-    isa: InstructionSetArchitecture,
+    isa: TargetDevice,
     config: &Configuration,
 ) -> Result<NativeQuil, Error> {
     let endpoint = &config.quilc_url;
-    let params = QuilcParams::new(quil, isa)?;
+    let params = QuilcParams::new(quil, isa);
     let request = rpcq::RPCRequest::new("quil_to_native_quil", &params);
     rpcq::Client::new(endpoint)
         .map_err(|source| Error::from_quilc_error(endpoint.clone(), source))?
@@ -47,11 +50,14 @@ pub(crate) fn compile_program(
 
 /// All of the errors that can occur within this module.
 #[derive(Debug, thiserror::Error)]
-pub(crate) enum Error {
+pub enum Error {
+    /// An ISA-related error.
     #[error("Problem converting ISA to quilc format. This is a bug in this library or in QCS.")]
     Isa(#[from] isa::Error),
+    /// An error when trying to connect to quilc.
     #[error("Problem connecting to quilc at {0}")]
     QuilcConnection(String, #[source] rpcq::Error),
+    /// An error when trying to compile using quilc.
     #[error("Problem compiling quil program: {0}")]
     QuilcCompilation(String),
 }
@@ -83,6 +89,12 @@ impl From<NativeQuil> for String {
     }
 }
 
+impl AsRef<str> for NativeQuil {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
 /// A wrapper around [`Program`] which indicates it has been converted to `NativeQuil` (has been run
 /// through `quilc` and therefore is suitable to use on QPUs.
 #[derive(Debug, PartialEq, Clone)]
@@ -117,11 +129,11 @@ struct QuilcParams {
 }
 
 impl QuilcParams {
-    fn new(quil: &str, isa: InstructionSetArchitecture) -> Result<Self, Error> {
-        Ok(Self {
+    fn new(quil: &str, isa: TargetDevice) -> Self {
+        Self {
             protoquil: None,
-            args: [NativeQuilRequest::new(quil, isa)?],
-        })
+            args: [NativeQuilRequest::new(quil, isa)],
+        }
     }
 }
 
@@ -134,19 +146,19 @@ struct NativeQuilRequest {
 }
 
 impl NativeQuilRequest {
-    fn new(quil: &str, isa: InstructionSetArchitecture) -> Result<Self, Error> {
-        Ok(Self {
+    fn new(quil: &str, target_device: TargetDevice) -> Self {
+        Self {
             quil: String::from(quil),
-            target_device: TargetDevice::try_from(isa)?,
-        })
+            target_device,
+        }
     }
 }
 
 /// Description of a device to compile for, part of [`NativeQuilRequest`]
-#[derive(Serialize, Debug, Clone, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
 #[serde(tag = "_type")]
-struct TargetDevice {
-    isa: CompilerIsa,
+pub struct TargetDevice {
+    isa: Compiler,
     specs: HashMap<String, String>,
 }
 
@@ -155,7 +167,7 @@ impl TryFrom<InstructionSetArchitecture> for TargetDevice {
 
     fn try_from(isa: InstructionSetArchitecture) -> Result<Self, Self::Error> {
         Ok(Self {
-            isa: CompilerIsa::try_from(isa)?,
+            isa: Compiler::try_from(isa)?,
             specs: HashMap::new(),
         })
     }
@@ -163,11 +175,9 @@ impl TryFrom<InstructionSetArchitecture> for TargetDevice {
 
 #[cfg(test)]
 mod tests {
-    use std::fs::File;
-
-    use qcs_api::models::InstructionSetArchitecture;
-
     use super::*;
+    use qcs_api::models::InstructionSetArchitecture;
+    use std::fs::File;
 
     const EXPECTED_H0_OUTPUT: &str =
         "MEASURE 0                               # Entering/exiting rewiring: (#(0 1) . #(0 1))\n";
@@ -182,8 +192,12 @@ mod tests {
 
     #[test]
     fn compare_native_quil_to_expected_output() {
-        let output = compile_program("MEASURE 0", qvm_isa(), &Configuration::default())
-            .expect("Could not compile");
+        let output = compile_program(
+            "MEASURE 0",
+            TargetDevice::try_from(qvm_isa()).expect("Couldn't build target device from ISA"),
+            &Configuration::default(),
+        )
+        .expect("Could not compile");
         assert_eq!(String::from(output), EXPECTED_H0_OUTPUT);
     }
 
@@ -199,8 +213,12 @@ MEASURE 1 ro[1]
     #[tokio::test]
     async fn run_compiled_bell_state_on_qvm() {
         let config = Configuration::load().await.unwrap_or_default();
-        let output =
-            compile_program(BELL_STATE, aspen_9_isa(), &config).expect("Could not compile");
+        let output = compile_program(
+            BELL_STATE,
+            TargetDevice::try_from(aspen_9_isa()).expect("Couldn't build target device from ISA"),
+            &config,
+        )
+        .expect("Could not compile");
         let mut results = crate::qvm::Execution::new(&String::from(output))
             .unwrap()
             .run(10, &["ro"], &HashMap::default(), &config)
diff --git a/src/qpu/rewrite_arithmetic.rs b/crates/lib/src/qpu/rewrite_arithmetic.rs
similarity index 86%
rename from src/qpu/rewrite_arithmetic.rs
rename to crates/lib/src/qpu/rewrite_arithmetic.rs
index 190dab245..59a0620a0 100644
--- a/src/qpu/rewrite_arithmetic.rs
+++ b/crates/lib/src/qpu/rewrite_arithmetic.rs
@@ -1,4 +1,4 @@
-use std::convert::TryFrom;
+use std::{collections::HashMap, convert::TryFrom};
 
 use indexmap::set::IndexSet;
 use num::complex::Complex64;
@@ -12,7 +12,7 @@ use quil_rs::{
     Program,
 };
 
-use crate::qpu::quilc::NativeQuilProgram;
+use crate::{executable::Parameters, qpu::quilc::NativeQuilProgram};
 
 /// A function for converting `program` into a form that Rigetti QPUs can understand.
 ///
@@ -86,6 +86,70 @@ pub(crate) fn rewrite_arithmetic(program: Program) -> Result<(Program, Substitut
     ))
 }
 
+/// Take the user-provided map of [`Parameters`] and produce the map of substitutions which
+/// should be given to QCS with the executable.
+///
+/// # Example
+///
+/// If there was a Quil program:
+///
+/// ```quil
+/// DECLARE theta REAL
+///
+/// RX(theta) 0
+/// RX(theta + 1) 0
+/// RX(theta + 2) 0
+/// ```
+///
+/// It would be converted  (in [`Execution::new`]) to something like:
+///
+/// ```quil
+/// DECLARE __SUBST REAL[2]
+/// DECLARE theta REAL[1]
+///
+/// RX(theta) 0
+/// RX(__SUBST[0]) 0
+/// RX(__SUBST[1]) 0
+/// ```
+///
+/// Because QPUs do not evaluate expressions themselves. This function creates the values for
+/// `__SUBST` by calculating the original expressions given the user-provided params (in this
+/// case just `theta`).
+pub(crate) fn get_substitutions(
+    substitutions: &Substitutions,
+    params: &Parameters,
+) -> Result<Parameters, String> {
+    // Convert into the format that quil-rs expects.
+    let params: HashMap<&str, Vec<f64>> = params
+        .iter()
+        .map(|(key, value)| (key.as_ref(), value.clone()))
+        .collect();
+    let values = substitutions
+        .iter()
+        .map(|substitution: &Expression| {
+            substitution
+                .evaluate(&HashMap::new(), &params)
+                .map_err(|e| format!("Could not evaluate expression {}: {:?}", substitution, e))
+                .and_then(|complex| {
+                    if complex.im == 0.0 {
+                        Ok(complex.re)
+                    } else {
+                        Err(String::from(
+                            "Cannot substitute imaginary numbers for QPU execution",
+                        ))
+                    }
+                })
+        })
+        .collect::<Result<Vec<f64>, String>>()?;
+    // Convert back to the format that this library expects
+    let mut patch_values: Parameters = params
+        .into_iter()
+        .map(|(key, value)| (key.into(), value))
+        .collect();
+    patch_values.insert(SUBSTITUTION_NAME.into(), values);
+    Ok(patch_values)
+}
+
 /// All of the errors that can occur in this module.
 #[derive(Debug, thiserror::Error)]
 pub(crate) enum Error {
@@ -265,7 +329,7 @@ pub(crate) struct RewrittenProgram {
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
-pub(crate) struct RewrittenQuil(String);
+pub(crate) struct RewrittenQuil(pub String);
 
 impl From<RewrittenQuil> for String {
     fn from(quil: RewrittenQuil) -> String {
@@ -295,7 +359,9 @@ impl RewrittenProgram {
 mod describe_rewrite_arithmetic {
     use std::str::FromStr;
 
-    use super::*;
+    use quil_rs::Program;
+
+    use crate::qpu::rewrite_arithmetic::rewrite_arithmetic;
 
     #[test]
     fn it_substitutes_gate_parameters() {
diff --git a/src/qpu/rpcq.rs b/crates/lib/src/qpu/rpcq.rs
similarity index 99%
rename from src/qpu/rpcq.rs
rename to crates/lib/src/qpu/rpcq.rs
index 4c330dce5..98b5e7ebb 100644
--- a/src/qpu/rpcq.rs
+++ b/crates/lib/src/qpu/rpcq.rs
@@ -127,7 +127,7 @@ impl TryFrom<&EngagementWithCredentials> for Client {
 
 /// All of the possible errors for this module
 #[derive(Debug, thiserror::Error)]
-pub(crate) enum Error {
+pub enum Error {
     #[error("Could not create a socket.")]
     SocketCreation(#[source] zmq::Error),
     #[error("Failed while trying to set up auth. This is likely a bug in this library.")]
diff --git a/src/qpu/runner.rs b/crates/lib/src/qpu/runner.rs
similarity index 92%
rename from src/qpu/runner.rs
rename to crates/lib/src/qpu/runner.rs
index ef94624a6..6e9a31d7f 100644
--- a/src/qpu/runner.rs
+++ b/crates/lib/src/qpu/runner.rs
@@ -11,11 +11,12 @@ use serde_bytes::ByteBuf;
 
 use crate::executable::Parameters;
 
+use super::engagement;
 use super::rpcq::{Client, Error as RPCQError, RPCRequest};
 
 /// The QCS Job ID. Useful for debugging or retrieving results later.
 #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
-pub(crate) struct JobId(pub(crate) String);
+pub struct JobId(pub(crate) String);
 
 /// Execute compiled program on a QPU.
 pub(crate) fn submit(
@@ -41,13 +42,17 @@ pub(crate) fn retrieve_results(
 
 /// All of the possible errors for this module
 #[derive(Debug, thiserror::Error)]
-pub(crate) enum Error {
+pub enum Error {
     #[error("Error connecting to the QPU")]
     Connection(#[source] RPCQError),
+    #[error("Error getting engagement for QPU")]
+    Engagement(#[source] engagement::Error),
     #[error("An error not expected to occur—if encountered it may indicate bug in this library")]
     Unexpected(#[source] RPCQError),
     #[error("An error was returned from the QPU: {0}")]
     Qpu(String),
+    #[error("An error was encountered when claiming the client lock: {0}")]
+    ClientLock(String),
 }
 
 impl From<RPCQError> for Error {
@@ -123,27 +128,27 @@ impl<'request> From<&'request GetExecutionResultsRequest>
     }
 }
 
-#[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
-pub(crate) struct GetExecutionResultsResponse {
-    pub(crate) buffers: HashMap<String, Buffer>,
+#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
+pub struct GetExecutionResultsResponse {
+    pub buffers: HashMap<String, Buffer>,
     #[serde(default)]
-    pub(crate) execution_duration_microseconds: Option<u64>,
+    pub execution_duration_microseconds: Option<u64>,
 }
 
 /// The raw form of the data which comes back from an execution.
 ///
 /// Generally this should not be used directly, but converted into an appropriate
 /// 2-D array.
-#[derive(Deserialize, Debug, PartialEq, Clone, Eq)]
-pub(crate) struct Buffer {
-    shape: Vec<usize>,
-    data: ByteBuf,
-    dtype: DataType,
+#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Eq)]
+pub struct Buffer {
+    pub shape: Vec<usize>,
+    pub data: ByteBuf,
+    pub dtype: DataType,
 }
 
-#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
+#[derive(Deserialize, Serialize, Debug, Copy, Clone, PartialEq, Eq)]
 #[serde(rename_all = "lowercase")]
-pub(crate) enum DataType {
+pub enum DataType {
     Float64,
     Int16,
     Complex64,
@@ -287,11 +292,16 @@ impl TryFrom<Buffer> for Register {
 
 #[cfg(test)]
 mod describe_buffer {
-    use std::convert::TryInto;
+    use std::{collections::HashMap, convert::TryInto};
 
-    use crate::qpu::rpcq::RPCResponse;
+    use num::complex::Complex32;
+    use serde_bytes::ByteBuf;
 
-    use super::*;
+    use crate::qpu::{
+        rpcq::RPCResponse,
+        runner::{Buffer, DataType},
+        Register,
+    };
 
     #[test]
     fn it_converts_numpy_int16() {
@@ -375,7 +385,7 @@ mod describe_buffer {
             rmp_serde::from_read(data.as_slice()).unwrap();
         let mut buffers = match resp {
             RPCResponse::RPCReply { result, .. } => result,
-            _ => unreachable!(),
+            RPCResponse::RPCError { .. } => unreachable!(),
         };
         let q0 = buffers.remove("q0").expect("Could not find buffer q0");
         let q1 = buffers.remove("q1").expect("Could not find buffer q1");
diff --git a/src/qpu/translation.rs b/crates/lib/src/qpu/translation.rs
similarity index 99%
rename from src/qpu/translation.rs
rename to crates/lib/src/qpu/translation.rs
index 8c4e427cb..c393d902e 100644
--- a/src/qpu/translation.rs
+++ b/crates/lib/src/qpu/translation.rs
@@ -35,7 +35,7 @@ pub(crate) async fn translate(
 
 /// Errors that can occur during Translation
 #[derive(Debug, thiserror::Error)]
-pub(crate) enum Error {
+pub enum Error {
     #[error("Provided program could not be translated: {}", .0.message)]
     ProgramIssue(QcsError),
     #[error("Problem connecting to QCS")]
diff --git a/src/qvm/execution.rs b/crates/lib/src/qvm/execution.rs
similarity index 100%
rename from src/qvm/execution.rs
rename to crates/lib/src/qvm/execution.rs
diff --git a/src/qvm/mod.rs b/crates/lib/src/qvm/mod.rs
similarity index 99%
rename from src/qvm/mod.rs
rename to crates/lib/src/qvm/mod.rs
index 2c59c7b04..788291b8d 100644
--- a/src/qvm/mod.rs
+++ b/crates/lib/src/qvm/mod.rs
@@ -76,6 +76,6 @@ mod describe_request {
         assert_eq!(
             serde_json::from_str::<serde_json::Value>(&json_string).unwrap(),
             serde_json::json!({"type": "multishot", "addresses": {"ro": true}, "trials": 10, "quil-instructions": "H 0"})
-        )
+        );
     }
 }
diff --git a/src/register_data.rs b/crates/lib/src/register_data.rs
similarity index 99%
rename from src/register_data.rs
rename to crates/lib/src/register_data.rs
index b45e7297a..31a824d2c 100644
--- a/src/register_data.rs
+++ b/crates/lib/src/register_data.rs
@@ -127,8 +127,9 @@ fn transpose<T>(mut data: Vec<Vec<T>>, len: u16) -> Vec<Vec<T>> {
 #[cfg(test)]
 mod describe_program_results {
     use maplit::hashmap;
+    use num::complex::Complex32;
 
-    use super::*;
+    use crate::{qpu::Register, RegisterData};
 
     #[test]
     fn it_converts_from_i8_registers() {
diff --git a/crates/python/CHANGELOG.md b/crates/python/CHANGELOG.md
new file mode 100644
index 000000000..e4c7f8268
--- /dev/null
+++ b/crates/python/CHANGELOG.md
@@ -0,0 +1,12 @@
+## 0.1.0
+
+### Features
+This initial RC release exposes the following functions to Python:
+	- compile
+	- rewrite_arithmentic
+	- translate
+	- submit
+	- retrieve_results
+	- build_patch_values
+
+As this is a release candidate, they can be used experimentally to get a preview of upcoming functionality. Please report any issues and expect that these signatures and workflows are going to be changed before a full release.
diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml
new file mode 100644
index 000000000..0ab4f7bd2
--- /dev/null
+++ b/crates/python/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "qcs-sdk-python"
+description = "Python bindings to qcs-sdk-rust"
+version = "0.1.4"
+edition = "2021"
+license = "Apache-2.0"
+repository = "https://github.com/rigetti/qcs-sdk-rust"
+keywords = ["pyquil", "SDK", "Rigetti", "Quil", "Quantum"]
+categories = ["api-bindings", "compilers", "science", "emulators"]
+readme = "./README.md"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+name = "qcs_sdk"
+crate-type = ["cdylib"]
+
+[dependencies]
+qcs = { path = "../lib" }
+pyo3 = { version = "0.16", features = ["extension-module"] }
+pyo3-asyncio = { version = "0.16", features = ["tokio-runtime"] }
+pythonize = "0.16"
+tokio = "1.4"
+qcs-api = "0.2.1"
+serde_json = "1.0.85"
+
+[build-dependencies]
+pyo3-build-config = { version = "0.16" }
diff --git a/crates/python/README.md b/crates/python/README.md
new file mode 100644
index 000000000..4f31454f0
--- /dev/null
+++ b/crates/python/README.md
@@ -0,0 +1,9 @@
+# QCS SDK Python
+
+⚠️ In Development
+
+`qcs-sdk-python` provides an interface to Rigetti [Quantum Cloud Services](https://docs.rigetti.com/qcs/) (QCS), allowing users
+to compile and run Quil programs on Rigetti quantum processors. Internally, it is powered by the [QCS Rust SDK](https://github.com/rigetti/qcs-sdk-rust).
+
+While this package can be used directly, [pyQuil](https://pypi.org/project/pyquil/) offers more functionality and a 
+higher-level interface for building and executing Quil programs. This package is still in early development and breaking changes should be expected between minor versions.
diff --git a/crates/python/build.rs b/crates/python/build.rs
new file mode 100644
index 000000000..dace4a9ba
--- /dev/null
+++ b/crates/python/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+    pyo3_build_config::add_extension_module_link_args();
+}
diff --git a/crates/python/poetry.lock b/crates/python/poetry.lock
new file mode 100644
index 000000000..57ad83ad3
--- /dev/null
+++ b/crates/python/poetry.lock
@@ -0,0 +1,174 @@
+[[package]]
+name = "black"
+version = "22.8.0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "click"
+version = "8.1.3"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.5"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "maturin"
+version = "0.13.2"
+description = "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+zig = ["ziglang (>=0.9.0,<0.10.0)"]
+patchelf = ["patchelf"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pathspec"
+version = "0.10.0"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+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\"."
+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)"]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "typing-extensions"
+version = "4.3.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[metadata]
+lock-version = "1.1"
+python-versions = ">=3.8<3.10"
+content-hash = "e08cfb07ab22776bdaa7d25bd2277cca6672b6a5f60882473a0e1c7d75424bca"
+
+[metadata.files]
+black = [
+    {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"},
+    {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"},
+    {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"},
+    {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"},
+    {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"},
+    {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"},
+    {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"},
+    {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"},
+    {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"},
+    {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"},
+    {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"},
+    {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"},
+    {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"},
+    {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"},
+    {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"},
+    {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"},
+    {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"},
+    {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"},
+    {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"},
+    {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"},
+    {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"},
+    {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
+    {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
+]
+click = [
+    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+]
+colorama = [
+    {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
+    {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
+]
+maturin = [
+    {file = "maturin-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:57a870a82715ddd84e2f5a59cbb313c7d39ebf8398b2a0c36ada9ff9d82056d7"},
+    {file = "maturin-0.13.2-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:cc69d4c2854daa16b91bcc8bc0680a188eb4e239bd627dbc721c7177d2082b16"},
+    {file = "maturin-0.13.2-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ac51f99936dcbff9cbb8cef960c6b82203c7e4ea6cf2dc127df0c725ab1f73a6"},
+    {file = "maturin-0.13.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:ca3a3f2328a0da542323f75a725f38b8ebb575c5805b4bb5c9ba5dd046450239"},
+    {file = "maturin-0.13.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:5eb555636eb9fd4f54efc48e5891067f94c0b50f7f402a897c41644f817e70f4"},
+    {file = "maturin-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:93fef9c0ead64fb0741c18009dc0521181b91241d1c09b0eda5c9a5e06dfc6d0"},
+    {file = "maturin-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:0a37e30d71b24dfe6ca9eac639ef976bdbf6d1676605f7dd260b5e6e5f93d441"},
+    {file = "maturin-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:6f2421b23d6699f5d1b408735b306648277ccd833ea9496139f888f95a53faa0"},
+    {file = "maturin-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ddc42d0b51a2537257a74791df8fbdaf204e72e8e7e2e0da9e02d1c46c6699c"},
+    {file = "maturin-0.13.2-py3-none-win32.whl", hash = "sha256:7caf8c85b8cf759eca31e4c4b36b58afb28be5db86cce951d9633d99a70c4af1"},
+    {file = "maturin-0.13.2-py3-none-win_amd64.whl", hash = "sha256:66a46de95e919be8c5db0b4bc604617a1d79f031373afddb389049c4bbe5b064"},
+    {file = "maturin-0.13.2-py3-none-win_arm64.whl", hash = "sha256:4ed0ad394e50f5b43e2b4a79d7f395b1d98f7cd5b48a527fe6aeb0d4da15b803"},
+    {file = "maturin-0.13.2.tar.gz", hash = "sha256:18882138e633d32e7a18d147ec38c71930db254817f3714e9fd70e01e379e255"},
+]
+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"},
+]
+pathspec = [
+    {file = "pathspec-0.10.0-py3-none-any.whl", hash = "sha256:aefa80ac32d5bf1f96139dca67cefb69a431beff4e6bf1168468f37d7ab87015"},
+    {file = "pathspec-0.10.0.tar.gz", hash = "sha256:01eecd304ba0e6eeed188ae5fa568e99ef10265af7fd9ab737d6412b4ee0ab85"},
+]
+platformdirs = [
+    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
+    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
+]
+tomli = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+typing-extensions = [
+    {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
+    {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
+]
diff --git a/crates/python/pyproject.toml b/crates/python/pyproject.toml
new file mode 100644
index 000000000..fd047daf8
--- /dev/null
+++ b/crates/python/pyproject.toml
@@ -0,0 +1,50 @@
+# This is the metadata Maturin uploads to PyPI on publish
+[project]
+name = "qcs-sdk-python"
+version = "0.1.4"
+description = "Python interface for the QCS Rust SDK"
+readme = "README.md"
+license = { text = "Apache-2.0" }
+authors = [
+	{ name = "Rigetti Computing", email = "softapps@rigetti.com" },
+	{ name = "Mark Skilbeck", email = "mark.skilbeck@rigetti.com" },
+	{ name = "Marquess Valdez", email = "mvaldez@rigetti.com" },
+	{ name = "Randall Fulton", email = "rfulton@rigetti.com" },
+]
+classifiers = [
+	"Development Status :: 3 - Alpha",
+	"License :: OSI Approved :: Apache Software License",
+	"Programming Language :: Python :: 3.7",
+	"Programming Language :: Python :: 3.8",
+	"Programming Language :: Python :: 3.9",
+	"Operating System :: OS Independent",
+]
+
+# PEP 621 specifies the [project] table as the source for project metadata. However, Poetry only supports [tool.poetry]
+# 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.1.4"
+description = "Python interface for the QCS Rust SDK"
+readme = "README.md"
+authors = [
+	"Rigetti Computing <softapps@rigetti.com>",
+	"Mark Skilbeck <mark.skilbeck@rigetti.com>",
+	"Marquess Valdez <mvaldez@rigetti.com>",
+	"Randall Fulton <rfulton@rigetti.com>",
+]
+
+[tool.maturin]
+bindings = "pyo3"
+compatibility = "linux"
+sdist-include = ["README.md"]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+
+[tool.poetry.dev-dependencies]
+maturin = "^0.13.2"
+
+[build-system]
+requires = ["maturin>=0.13,<0.14"]
+build-backend = "maturin"
diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs
new file mode 100644
index 000000000..1e4c9a6ef
--- /dev/null
+++ b/crates/python/src/lib.rs
@@ -0,0 +1,120 @@
+use ::qcs::configuration::Configuration;
+use pythonize::pythonize;
+use qcs::api;
+use qcs::qpu::quilc::TargetDevice;
+use std::collections::HashMap;
+
+use pyo3::{create_exception, exceptions::PyRuntimeError, prelude::*};
+
+create_exception!(qcs, InvalidConfigError, PyRuntimeError);
+create_exception!(qcs, ExecutionError, PyRuntimeError);
+create_exception!(qcs, TranslationError, PyRuntimeError);
+create_exception!(qcs, CompilationError, PyRuntimeError);
+
+#[pyfunction]
+fn compile(py: Python<'_>, quil: String, target_device: String) -> PyResult<&PyAny> {
+    let target_device: TargetDevice = serde_json::from_str(&target_device)
+        .map_err(|e| CompilationError::new_err(e.to_string()))?;
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let config = Configuration::load()
+            .await
+            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
+        let result = api::compile(&quil, target_device, &config)
+            .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 result = api::rewrite_arithmetic(&native_quil).map_err(TranslationError::new_err)?;
+    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 config = Configuration::load()
+            .await
+            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
+        let result = api::translate(&native_quil, num_shots, &quantum_processor_id, &config)
+            .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,
+) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let config = Configuration::load()
+            .await
+            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
+        let job_id = api::submit(&program, patch_values, &quantum_processor_id, &config)
+            .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,
+) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let config = Configuration::load()
+            .await
+            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
+        let results = api::retrieve_results(&job_id, &quantum_processor_id, &config)
+            .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)
+    })
+}
+
+#[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(())
+}
diff --git a/deny.toml b/deny.toml
index 260b9f6fa..2879cef42 100644
--- a/deny.toml
+++ b/deny.toml
@@ -37,6 +37,7 @@ allow = [
     "MIT",
     "OpenSSL",
     "BSD-3-Clause",
+    "Unicode-DFS-2016",
 ]
 # List of explictly disallowed licenses
 # See https://spdx.org/licenses/ for list of possible licenses
diff --git a/knope.toml b/knope.toml
index db740d2fc..d6369c0ef 100644
--- a/knope.toml
+++ b/knope.toml
@@ -1,6 +1,12 @@
-[[packages]]
-versioned_files = ["Cargo.toml"]
-changelog = "CHANGELOG.md"
+[packages.lib]
+versioned_files = ["crates/lib/Cargo.toml"]
+changelog = "crates/lib/CHANGELOG.md"
+scopes = ["lib", "rust"]
+
+[packages.python]
+versioned_files = ["crates/python/Cargo.toml", "crates/python/pyproject.toml"]
+changelog = "crates/python/CHANGELOG.md"
+scopes = ["python"]
 
 [[workflows]]
 name = "release"
@@ -14,10 +20,29 @@ command = "cargo update -w"
 
 [[workflows.steps]]
 type = "Command"
-command = "git add Cargo.toml Cargo.lock CHANGELOG.md && git commit -m \"chore: prepare release $version\" && git push"
+command = "git add Cargo.lock && git commit -m \"chore: prepare new release(s)\""
+
+[[workflows.steps]]
+type = "Command"
+command = "git push"
+
+[[workflows.steps]]
+type = "Release"
+
+[[workflows]]
+name = "prerelease"
 
-[workflows.steps.variables]
-"$version" = "Version"
+[[workflows.steps]]
+type = "PrepareRelease"
+prerelease_label = "rc"
+
+[[workflows.steps]]
+type = "Command"
+command = "cargo update -w && git add Cargo.lock && git commit -m \"chore: prepare new prerelease(s)\""
+
+[[workflows.steps]]
+type = "Command"
+command = "git push"
 
 [[workflows.steps]]
 type = "Release"
diff --git a/src/qpu/quilc/isa/operator.rs b/src/qpu/quilc/isa/operator.rs
deleted file mode 100644
index e5e3ee74f..000000000
--- a/src/qpu/quilc/isa/operator.rs
+++ /dev/null
@@ -1,316 +0,0 @@
-use std::collections::HashSet;
-
-use serde::ser::SerializeSeq;
-use serde::{Serialize, Serializer};
-
-/// Contains all the operators for a single Site ([`super::qubit::Qubit`] or [`super::edge::Edge`]) organized to allow
-/// deduplication by name
-#[derive(Debug, Clone, PartialEq)]
-pub(crate) struct OperatorMap(HashSet<&'static str>, Vec<Operator>);
-
-impl OperatorMap {
-    pub(crate) fn new() -> Self {
-        Self(HashSet::new(), Vec::new())
-    }
-
-    /// Add a new batch of operators with a given name if no operators with that name have been
-    /// added yet.
-    ///
-    /// # Arguments
-    ///
-    /// * `name`: The name of the operators being added.
-    /// * `operators`: The list of operators to add.
-    ///
-    /// returns: true if the operators were inserted, false if they weren't (due to duplicates).
-    pub(crate) fn add(&mut self, operators: &[Operator]) -> bool {
-        if operators.is_empty() {
-            return false;
-        }
-        let name = operators[0].name();
-        if !operators.iter().all(|op| op.name() == name) {
-            return false;
-        }
-        if !self.0.insert(name) {
-            return false;
-        }
-        self.1.extend(operators);
-        true
-    }
-
-    /// Works just like [`Self::add`] but it only adds a single operator.
-    pub(crate) fn add_one(&mut self, operator: Operator) -> bool {
-        if !self.0.insert(operator.name()) {
-            return false;
-        }
-        self.1.push(operator);
-        true
-    }
-}
-
-impl Serialize for OperatorMap {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        serializer.collect_seq(&self.1)
-    }
-}
-
-#[cfg(test)]
-mod describe_operator_map {
-    use super::*;
-
-    #[test]
-    fn it_serializes_as_a_list_of_operators() {
-        let mut map = OperatorMap::new();
-
-        let rx_1 = Operator::Gate {
-            operator: "RX",
-            duration: 50.0,
-            fidelity: 1.0,
-            parameters: Parameters::Float(0.0),
-            arguments: Arguments::Int(1),
-        };
-        let rx_2 = Operator::Gate {
-            operator: "RX",
-            duration: 50.0,
-            fidelity: 0.9,
-            parameters: Parameters::Float(0.9),
-            arguments: Arguments::Int(1),
-        };
-        map.add(&[rx_1.clone(), rx_2.clone()]);
-        let rz = Operator::Gate {
-            operator: "RZ",
-            duration: 0.01,
-            fidelity: 0.9,
-            parameters: Parameters::Underscore,
-            arguments: Arguments::Int(1),
-        };
-        map.add(&[rz]);
-        let serialized = serde_json::to_value(&map).expect("Could not serialize OperatorMap");
-        let expected =
-            serde_json::to_value([rx_1, rx_2, rz]).expect("Could not serialize vec of operators");
-        assert_eq!(serialized, expected);
-    }
-
-    #[test]
-    fn it_skips_duplicate_operators_with_the_same_name() {
-        let mut map = OperatorMap::new();
-
-        let rz = Operator::Gate {
-            operator: "RZ",
-            duration: 0.01,
-            fidelity: 0.9,
-            parameters: Parameters::Underscore,
-            arguments: Arguments::Int(1),
-        };
-        map.add(&[rz]);
-        map.add(&[rz]);
-        let serialized = serde_json::to_value(&map).expect("Could not serialize OperatorMap");
-        let expected =
-            serde_json::to_value(vec![rz]).expect("Could not serialize vec of operators");
-        assert_eq!(serialized, expected);
-    }
-}
-
-/// Represents a single operation that can be performed on a Qubit or Edge
-#[derive(Serialize, Debug, Clone, Copy, PartialEq)]
-#[serde(tag = "operator_type")]
-#[serde(rename_all = "lowercase")]
-pub(crate) enum Operator {
-    Gate {
-        operator: &'static str,
-        duration: f64,
-        fidelity: f64,
-        parameters: Parameters,
-        arguments: Arguments,
-    },
-    Measure {
-        operator: &'static str,
-        duration: f64,
-        fidelity: f64,
-        qubit: i32,
-        target: Option<&'static str>,
-    },
-}
-
-impl Operator {
-    pub(crate) fn name(&self) -> &'static str {
-        match self {
-            Operator::Gate { operator, .. } | Operator::Measure { operator, .. } => operator,
-        }
-    }
-}
-
-#[cfg(test)]
-mod describe_operator {
-    use super::*;
-
-    /// This test copies some JSON data from the pyQuil ISA integration test to validate that
-    /// Operator::Gate is serialized correctly.
-    #[test]
-    fn it_serializes_gates_like_pyquil() {
-        let gate_op = Operator::Gate {
-            arguments: Arguments::Int(1),
-            duration: 0.5,
-            fidelity: 0.5,
-            operator: "RZ",
-            parameters: Parameters::Underscore,
-        };
-        let expected = serde_json::json!({
-            "arguments": [1],
-            "duration": 0.5,
-            "fidelity": 0.5,
-            "operator": "RZ",
-            "operator_type": "gate",
-            "parameters": ["_"]
-        });
-        let serialized =
-            serde_json::to_value(&gate_op).expect("Could not serialize Operation::Gate");
-        assert_eq!(serialized, expected);
-    }
-
-    /// This test copies some JSON data from the pyQuil ISA integration test to validate that
-    /// Operator::Measure is serialized correctly.
-    #[test]
-    fn it_serializes_measurements_like_pyquil() {
-        let measure = Operator::Measure {
-            duration: 0.5,
-            fidelity: 0.5,
-            qubit: 1,
-            operator: "MEASURE",
-            target: Some("_"),
-        };
-        let expected = serde_json::json!({
-            "duration": 0.5,
-            "fidelity": 0.5,
-            "operator": "MEASURE",
-            "operator_type": "measure",
-            "qubit": 1,
-            "target": "_"
-        });
-        let serialized =
-            serde_json::to_value(&measure).expect("Could not serialize Operation::Gate");
-        assert_eq!(serialized, expected);
-    }
-
-    /// This test copies some JSON data from the pyQuil ISA integration test to validate that
-    /// Operator::Measure is serialized correctly.
-    #[test]
-    fn it_serializes_measurements_with_null_targets_like_pyquil() {
-        let measure = Operator::Measure {
-            duration: 0.5,
-            fidelity: 0.5,
-            qubit: 1,
-            operator: "MEASURE",
-            target: None,
-        };
-        let expected = serde_json::json!({
-            "duration": 0.5,
-            "fidelity": 0.5,
-            "operator": "MEASURE",
-            "operator_type": "measure",
-            "qubit": 1,
-            "target": null
-        });
-        let serialized =
-            serde_json::to_value(&measure).expect("Could not serialize Operation::Gate");
-        assert_eq!(serialized, expected);
-    }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub(crate) enum Parameters {
-    Underscore,
-    Theta,
-    Float(f64),
-    Empty,
-}
-
-const UNDERSCORE: [&str; 1] = ["_"];
-const THETA: [&str; 1] = ["theta"];
-
-impl Serialize for Parameters {
-    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        match self {
-            Self::Underscore => serializer.collect_seq(UNDERSCORE),
-            Self::Theta => serializer.collect_seq(THETA),
-            Self::Float(element) => serializer.collect_seq([element]),
-            Self::Empty => serializer.collect_seq(Vec::new() as Vec<i32>),
-        }
-    }
-}
-
-#[cfg(test)]
-mod describe_parameters {
-    use super::*;
-
-    #[test]
-    fn it_serializes_underscore_as_list_of_strings() {
-        let p = Parameters::Underscore;
-        let serialized = serde_json::to_value(p).expect("Could not serialize");
-        let expected = serde_json::json!(["_"]);
-        assert_eq!(expected, serialized);
-    }
-
-    #[test]
-    fn it_serializes_one_float_as_list_of_numbers() {
-        let p = Parameters::Float(1.0);
-        let serialized = serde_json::to_value(p).expect("Could not serialize");
-        let expected = serde_json::json!([1.0]);
-        assert_eq!(expected, serialized);
-    }
-
-    #[test]
-    fn it_serializes_empty_as_sequence() {
-        let p = Parameters::Empty;
-        let serialized = serde_json::to_value(p).expect("Could not serialize");
-        let expected = serde_json::json!([]);
-        assert_eq!(expected, serialized);
-    }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum Arguments {
-    Int(i32),
-    Underscores,
-}
-
-const UNDERSCORES: [&str; 2] = ["_", "_"];
-
-impl Serialize for Arguments {
-    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        match self {
-            Self::Underscores => serializer.collect_seq(UNDERSCORES),
-            Self::Int(int) => {
-                let mut seq = serializer.serialize_seq(Some(1))?;
-                seq.serialize_element(int)?;
-                seq.end()
-            }
-        }
-    }
-}
-
-#[cfg(test)]
-mod describe_arguments {
-    use super::*;
-
-    #[test]
-    fn it_serializes_underscores_as_list_of_strings() {
-        let p = Arguments::Underscores;
-        let serialized = serde_json::to_value(p).expect("Could not serialize");
-        let expected = serde_json::json!(["_", "_"]);
-        assert_eq!(expected, serialized);
-    }
-
-    #[test]
-    fn it_serializes_one_int_as_list_of_numbers() {
-        let p = Arguments::Int(1);
-        let serialized = serde_json::to_value(p).expect("Could not serialize");
-        let expected = serde_json::json!([1]);
-        assert_eq!(expected, serialized);
-    }
-}
-
-pub(crate) const PERFECT_FIDELITY: f64 = 1.0;
-pub(crate) const PERFECT_DURATION: f64 = 1.0 / 100.0;


Test patch:
diff --git a/tests/aspen_9_isa.json b/crates/lib/tests/aspen_9_isa.json
similarity index 100%
rename from tests/aspen_9_isa.json
rename to crates/lib/tests/aspen_9_isa.json
diff --git a/tests/basic_qvm.rs b/crates/lib/tests/basic_qvm.rs
similarity index 100%
rename from tests/basic_qvm.rs
rename to crates/lib/tests/basic_qvm.rs
diff --git a/tests/bell_state_response_data.hex b/crates/lib/tests/bell_state_response_data.hex
similarity index 100%
rename from tests/bell_state_response_data.hex
rename to crates/lib/tests/bell_state_response_data.hex
diff --git a/tests/compiler-isa-Aspen-8.json b/crates/lib/tests/compiler-isa-Aspen-8.json
similarity index 100%
rename from tests/compiler-isa-Aspen-8.json
rename to crates/lib/tests/compiler-isa-Aspen-8.json
diff --git a/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs
similarity index 99%
rename from tests/mocked_qpu.rs
rename to crates/lib/tests/mocked_qpu.rs
index 5e4ff4fae..da4d601ea 100644
--- a/tests/mocked_qpu.rs
+++ b/crates/lib/tests/mocked_qpu.rs
@@ -204,14 +204,14 @@ mod qpu {
         RPCError { id: String, error: String },
     }
 
-    #[derive(Serialize, Debug, PartialEq)]
+    #[derive(Serialize, Debug, PartialEq, Eq)]
     pub struct Buffer {
         shape: (usize,),
         data: [u8; 2],
         dtype: String,
     }
 
-    #[derive(Serialize, Debug, PartialEq)]
+    #[derive(Serialize, Debug, PartialEq, Eq)]
     pub struct GetExecutionResultsResponse {
         buffers: HashMap<&'static str, Buffer>,
         execution_duration_microseconds: u64,
diff --git a/tests/parametric_compilation.rs b/crates/lib/tests/parametric_compilation.rs
similarity index 93%
rename from tests/parametric_compilation.rs
rename to crates/lib/tests/parametric_compilation.rs
index 49685283f..4abe205c6 100644
--- a/tests/parametric_compilation.rs
+++ b/crates/lib/tests/parametric_compilation.rs
@@ -29,12 +29,12 @@ async fn basic_substitution() {
             .with_parameter("theta", 0, theta)
             .execute_on_qvm()
             .await
-            .expect("Failed to execute");
+            .expect("Executed on QPU");
         parametric_measurements.append(
             &mut result
                 .registers
                 .remove("ro")
-                .expect("Missing ro register")
+                .expect("Found ro register")
                 .into_i8()
                 .unwrap()[0],
         )
diff --git a/tests/qcs-isa-Aspen-8.json b/crates/lib/tests/qcs-isa-Aspen-8.json
similarity index 100%
rename from tests/qcs-isa-Aspen-8.json
rename to crates/lib/tests/qcs-isa-Aspen-8.json
diff --git a/tests/qvm_isa.json b/crates/lib/tests/qvm_isa.json
similarity index 100%
rename from tests/qvm_isa.json
rename to crates/lib/tests/qvm_isa.json
diff --git a/tests/secrets.toml b/crates/lib/tests/secrets.toml
similarity index 100%
rename from tests/secrets.toml
rename to crates/lib/tests/secrets.toml
diff --git a/tests/settings.toml b/crates/lib/tests/settings.toml
similarity index 100%
rename from tests/settings.toml
rename to crates/lib/tests/settings.toml
diff --git a/crates/python/test.py b/crates/python/test.py
new file mode 100644
index 000000000..8f891745f
--- /dev/null
+++ b/crates/python/test.py
@@ -0,0 +1,29 @@
+import os
+import asyncio
+import numpy as np
+import qcs
+
+QPID = os.getenv("QPID", "Aspen-11")
+
+program = """
+DECLARE ro BIT
+DECLARE theta REAL
+RX(theta) 0
+MEASURE 0 ro[0]
+"""
+
+async def main():
+    print(f"Executing program on {QPID}.")
+    try:
+        native_quil = await qcs.compile(program, QPID)
+        translated = await qcs.translate(native_quil, 1, QPID)
+        print(translated)
+        job_id = await qcs.submit(translated['program'], {'theta': [np.pi]}, QPID)
+        print(job_id)
+        results = await qcs.retrieve_results(job_id, QPID)
+        print(results)
+    except Exception as e:
+        print(e)
+
+
+asyncio.run(main())