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

Issue:
feat: the default QCS Client configuration now respects environment variable overrides
As of `qcs-api-client-common` `0.6.6`, `ClientConfiguration` respects the value of environment variable overrides as the default value. Including these changes here allows users to initialize a default `QCSClient` that doesn't require a full blown QCS configuration but still uses overrides from the environment. This is useful for users who just want to use `quilc` and/or the `qvm`. They may not have a QCS config, but they still may set the `QCS_APPLICATIONS_QVM_URL` or `QCS_APPLICATIONS_QUILC_URL` environment variables and expect them to work.


Discussion / hints:

Fix patch:
diff --git a/crates/lib/src/qpu/client.rs b/crates/lib/src/client.rs
similarity index 88%
rename from crates/lib/src/qpu/client.rs
rename to crates/lib/src/client.rs
index f72006771..ec2129d85 100644
--- a/crates/lib/src/qpu/client.rs
+++ b/crates/lib/src/client.rs
@@ -43,17 +43,17 @@ pub struct Qcs {
 
 impl Qcs {
     /// Create a [`Qcs`] and initialize it with the user's default [`ClientConfiguration`]
-    pub async fn load() -> Result<Self, LoadError> {
-        ClientConfiguration::load_default()
-            .await
-            .or_else(|_| {
-                #[cfg(feature = "tracing")]
-                tracing::info!(
-                    "No QCS client configuration found. QPU data and QCS will be inaccessible and only generic QVMs will be available for execution"
-                );
-                Ok(ClientConfiguration::default())
-            })
-            .map(Self::with_config)
+    pub async fn load() -> Self {
+        let config = if let Ok(config) = ClientConfiguration::load_default().await {
+            config
+        } else {
+            #[cfg(feature = "tracing")]
+            tracing::info!(
+                "No QCS client configuration found. QPU data and QCS will be inaccessible and only generic QVMs will be available for execution"
+            );
+            ClientConfiguration::default()
+        };
+        Self::with_config(config)
     }
 
     /// Create a [`Qcs`] and initialize it with the given [`ClientConfiguration`]
@@ -65,6 +65,18 @@ impl Qcs {
         }
     }
 
+    /// Create a [`Qcs`] and initialized with the given `profile`.
+    ///
+    /// # Errors
+    ///
+    /// A [`LoadError`] will be returned if QCS credentials are
+    /// not correctly configured or the given profile is not defined.
+    pub async fn with_profile(profile: String) -> Result<Qcs, LoadError> {
+        ClientConfiguration::load_profile(profile)
+            .await
+            .map(Self::with_config)
+    }
+
     /// Enable or disable the use of Gateway service for execution
     #[must_use]
     pub fn with_use_gateway(mut self, use_gateway: bool) -> Self {
@@ -72,10 +84,10 @@ impl Qcs {
         self
     }
 
-    /// Return a copy of all settings parsed and resolved from configuration sources.
+    /// Return a reference to the underlying [`ClientConfiguration`] with all settings parsed and resolved from configuration sources.
     #[must_use]
-    pub fn get_config(&self) -> ClientConfiguration {
-        self.config.clone()
+    pub fn get_config(&self) -> &ClientConfiguration {
+        &self.config
     }
 
     pub(crate) async fn get_controller_client(
@@ -85,7 +97,7 @@ impl Qcs {
     {
         let uri = self.get_controller_endpoint(quantum_processor_id).await?;
         let channel = get_channel(uri).map_err(|err| GrpcEndpointError::GrpcError(err.into()))?;
-        let service = wrap_channel_with(channel, self.get_config());
+        let service = wrap_channel_with(channel, self.get_config().clone());
         Ok(ControllerClient::new(service))
     }
 
@@ -96,12 +108,12 @@ impl Qcs {
     {
         let uri = self.get_controller_endpoint_by_id(endpoint_id).await?;
         let channel = get_channel(uri).map_err(|err| GrpcEndpointError::GrpcError(err.into()))?;
-        let service = wrap_channel_with(channel, self.get_config());
+        let service = wrap_channel_with(channel, self.get_config().clone());
         Ok(ControllerClient::new(service))
     }
 
     pub(crate) fn get_openapi_client(&self) -> OpenApiConfiguration {
-        OpenApiConfiguration::with_qcs_config(self.get_config())
+        OpenApiConfiguration::with_qcs_config(self.get_config().clone())
     }
 
     pub(crate) fn get_translation_client(
@@ -122,7 +134,7 @@ impl Qcs {
     > {
         let uri = parse_uri(translation_grpc_endpoint)?;
         let channel = get_channel(uri)?;
-        let service = wrap_channel_with(channel, self.get_config());
+        let service = wrap_channel_with(channel, self.get_config().clone());
         Ok(TranslationClient::new(service))
     }
 
diff --git a/crates/lib/src/compiler/quilc.rs b/crates/lib/src/compiler/quilc.rs
index da4694b43..9395f0cb8 100644
--- a/crates/lib/src/compiler/quilc.rs
+++ b/crates/lib/src/compiler/quilc.rs
@@ -12,7 +12,7 @@ use qcs_api_client_openapi::models::InstructionSetArchitecture;
 use super::isa::{self, Compiler};
 use super::rpcq;
 
-use crate::qpu::client::Qcs;
+use crate::client::Qcs;
 
 /// Number of seconds to wait before timing out.
 pub const DEFAULT_COMPILER_TIMEOUT: f64 = 30.0;
@@ -47,8 +47,7 @@ pub fn compile_program(
     #[cfg(feature = "tracing")]
     tracing::debug!(compiler_options=?options, "compiling quil program with quilc",);
 
-    let config = client.get_config();
-    let endpoint = config.quilc_url();
+    let endpoint = client.get_config().quilc_url();
     let params = QuilcParams::new(quil, isa).with_protoquil(options.protoquil);
     let request =
         rpcq::RPCRequest::new("quil_to_native_quil", &params).with_timeout(options.timeout);
@@ -428,7 +427,7 @@ mod tests {
         let output = compile_program(
             "MEASURE 0",
             TargetDevice::try_from(qvm_isa()).expect("Couldn't build target device from ISA"),
-            &Qcs::load().await.unwrap_or_default(),
+            &Qcs::load().await,
             CompilerOpts::default(),
         )
         .expect("Could not compile");
@@ -446,7 +445,7 @@ MEASURE 1 ro[1]
 
     #[tokio::test]
     async fn run_compiled_bell_state_on_qvm() {
-        let client = Qcs::load().await.unwrap_or_default();
+        let client = Qcs::load().await;
         let output = compile_program(
             BELL_STATE,
             TargetDevice::try_from(aspen_9_isa()).expect("Couldn't build target device from ISA"),
@@ -456,12 +455,7 @@ MEASURE 1 ro[1]
         .expect("Could not compile");
         let mut results = crate::qvm::Execution::new(&output.program.to_string(true))
             .unwrap()
-            .run(
-                10,
-                &[Cow::Borrowed("ro")],
-                &HashMap::default(),
-                &client.get_config(),
-            )
+            .run(10, &[Cow::Borrowed("ro")], &HashMap::default(), &client)
             .await
             .expect("Could not run program on QVM");
         for shot in results
@@ -478,7 +472,7 @@ MEASURE 1 ro[1]
 
     #[tokio::test]
     async fn test_compile_declare_only() {
-        let client = Qcs::load().await.unwrap_or_default();
+        let client = Qcs::load().await;
         let output = compile_program(
             "DECLARE ro BIT[1]\n",
             TargetDevice::try_from(aspen_9_isa()).expect("Couldn't build target device from ISA"),
@@ -492,7 +486,7 @@ MEASURE 1 ro[1]
 
     #[tokio::test]
     async fn get_version_info_from_quilc() {
-        let client = Qcs::load().await.unwrap_or_default();
+        let client = Qcs::load().await;
         let version = get_version_info(&client).expect("Should get version info from quilc");
         let semver_re = Regex::new(r"^([0-9]+)\.([0-9]+)\.([0-9]+)$").unwrap();
         assert!(semver_re.is_match(&version));
@@ -500,7 +494,7 @@ MEASURE 1 ro[1]
 
     #[tokio::test]
     async fn test_conjugate_pauli_by_clifford() {
-        let client = Qcs::load().await.unwrap_or_default();
+        let client = Qcs::load().await;
         let request = ConjugateByCliffordRequest {
             pauli: PauliTerm {
                 indices: vec![0],
@@ -522,7 +516,7 @@ MEASURE 1 ro[1]
 
     #[tokio::test]
     async fn test_generate_randomized_benchmark_sequence() {
-        let client = Qcs::load().await.unwrap_or_default();
+        let client = Qcs::load().await;
         let request = RandomizedBenchmarkingRequest {
             depth: 2,
             qubits: 1,
diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs
index 7474991c9..49e881438 100644
--- a/crates/lib/src/executable.rs
+++ b/crates/lib/src/executable.rs
@@ -7,13 +7,12 @@ use std::sync::Arc;
 use std::time::Duration;
 
 use qcs_api_client_common::configuration::LoadError;
-use qcs_api_client_common::ClientConfiguration;
 use qcs_api_client_grpc::services::translation::TranslationOptions;
 
+use crate::client::Qcs;
 use crate::compiler::quilc::CompilerOpts;
 use crate::execution_data::{self, ResultData};
 use crate::qpu::api::{JobId, JobTarget};
-use crate::qpu::client::Qcs;
 use crate::qpu::rewrite_arithmetic;
 use crate::qpu::ExecutionError;
 use crate::{qpu, qvm};
@@ -25,7 +24,7 @@ use quil_rs::Program;
 /// # Example
 ///
 /// ```rust
-/// use qcs_api_client_common::ClientConfiguration;
+/// use qcs::client::Qcs;
 /// use qcs::Executable;
 ///
 ///
@@ -41,7 +40,7 @@ use quil_rs::Program;
 ///
 /// #[tokio::main]
 /// async fn main() {
-///     let mut result = Executable::from_quil(PROGRAM).with_config(ClientConfiguration::default()).with_shots(4).execute_on_qvm().await.unwrap();
+///     let mut result = Executable::from_quil(PROGRAM).with_client(Qcs::default()).with_shots(4).execute_on_qvm().await.unwrap();
 ///     // "ro" is the only source read from by default if you don't specify a .read_from()
 ///
 ///     // We first convert the readout data to a [`RegisterMap`] to get a mapping of registers
@@ -87,7 +86,6 @@ pub struct Executable<'executable, 'execution> {
     params: Parameters,
     compile_with_quilc: bool,
     compiler_options: CompilerOpts,
-    config: Option<ClientConfiguration>,
     client: Option<Arc<Qcs>>,
     qpu: Option<qpu::Execution<'execution>>,
     qvm: Option<qvm::Execution>,
@@ -119,7 +117,6 @@ impl<'executable> Executable<'executable, '_> {
             params: Parameters::new(),
             compile_with_quilc: true,
             compiler_options: CompilerOpts::default(),
-            config: None,
             client: None,
             qpu: None,
             qvm: None,
@@ -140,7 +137,7 @@ impl<'executable> Executable<'executable, '_> {
     /// # Example
     ///
     /// ```rust
-    /// use qcs_api_client_common::ClientConfiguration;
+    /// use qcs::client::Qcs;
     /// use qcs::Executable;
     ///
     /// const PROGRAM: &str = r#"
@@ -154,7 +151,7 @@ impl<'executable> Executable<'executable, '_> {
     /// #[tokio::main]
     /// async fn main() {
     ///     let mut result = Executable::from_quil(PROGRAM)
-    ///         .with_config(ClientConfiguration::default()) // Unnecessary if you have ~/.qcs/settings.toml
+    ///         .with_client(Qcs::default()) // Unnecessary if you have ~/.qcs/settings.toml
     ///         .read_from("first")
     ///         .read_from("second")
     ///         .execute_on_qvm()
@@ -214,7 +211,7 @@ impl<'executable> Executable<'executable, '_> {
     /// # Example
     ///
     /// ```rust
-    /// use qcs_api_client_common::ClientConfiguration;
+    /// use qcs::client::Qcs;
     /// use qcs::Executable;
     ///
     /// const PROGRAM: &str = "DECLARE theta REAL[2]";
@@ -222,7 +219,7 @@ impl<'executable> Executable<'executable, '_> {
     /// #[tokio::main]
     /// async fn main() {
     ///     let mut exe = Executable::from_quil(PROGRAM)
-    ///         .with_config(ClientConfiguration::default()) // Unnecessary if you have ~/.qcs/settings.toml
+    ///         .with_client(Qcs::default()) // Unnecessary if you have ~/.qcs/settings.toml
     ///         .read_from("theta");
     ///
     ///     for theta in 0..2 {
@@ -285,8 +282,8 @@ impl<'executable> Executable<'executable, '_> {
 
     /// Set the default configuration to be used when constructing clients
     #[must_use]
-    pub fn with_config(mut self, config: ClientConfiguration) -> Self {
-        self.config = Some(config);
+    pub fn with_client(mut self, client: Qcs) -> Self {
+        self.client = Some(Arc::from(client));
         self
     }
 }
@@ -345,7 +342,7 @@ impl Executable<'_, '_> {
             "running Executable on QVM",
         );
 
-        let config = self.get_config().await?;
+        let client = self.get_client().await?;
 
         let mut qvm = if let Some(qvm) = self.qvm.take() {
             qvm
@@ -353,7 +350,7 @@ impl Executable<'_, '_> {
             qvm::Execution::new(&self.quil)?
         };
         let result = qvm
-            .run(self.shots, self.get_readouts(), &self.params, &config)
+            .run(self.shots, self.get_readouts(), &self.params, &client)
             .await;
         self.qvm = Some(qvm);
         result
@@ -364,23 +361,12 @@ impl Executable<'_, '_> {
             })
     }
 
-    async fn get_config(&mut self) -> Result<ClientConfiguration, Error> {
-        if let Some(config) = &self.config {
-            Ok(config.clone())
-        } else {
-            let config = ClientConfiguration::load_default().await?;
-            self.config = Some(config.clone());
-            Ok(config)
-        }
-    }
-
     /// Load `self.client` if not yet loaded, then return a reference to it.
     async fn get_client(&mut self) -> Result<Arc<Qcs>, Error> {
         if let Some(client) = &self.client {
             Ok(client.clone())
         } else {
-            let config = self.get_config().await?;
-            let client = Arc::new(Qcs::with_config(config));
+            let client = Arc::new(Qcs::load().await);
             self.client = Some(client.clone());
             Ok(client)
         }
@@ -478,7 +464,11 @@ impl<'execution> Executable<'_, 'execution> {
     /// 1. Missing parameters that should be filled with [`Executable::with_parameter`]
     ///
     /// [quilc]: https://github.com/quil-lang/quilc
-    pub async fn execute_on_qpu<S>(&mut self, quantum_processor_id: S, translation_options: Option<TranslationOptions>) -> ExecutionResult
+    pub async fn execute_on_qpu<S>(
+        &mut self,
+        quantum_processor_id: S,
+        translation_options: Option<TranslationOptions>,
+    ) -> ExecutionResult
     where
         S: Into<Cow<'execution, str>>,
     {
@@ -491,7 +481,9 @@ impl<'execution> Executable<'_, 'execution> {
             "running Executable on QPU",
         );
 
-        let job_handle = self.submit_to_qpu(quantum_processor_id, translation_options).await?;
+        let job_handle = self
+            .submit_to_qpu(quantum_processor_id, translation_options)
+            .await?;
         self.retrieve_results(job_handle).await
     }
 
@@ -741,13 +733,16 @@ impl<'a> JobHandle<'a> {
 
 #[cfg(test)]
 mod describe_get_config {
+    #[cfg(feature = "manual-tests")]
+    use crate::client::Qcs;
+    #[cfg(feature = "manual-tests")]
     use qcs_api_client_openapi::common::ClientConfiguration;
 
     use crate::Executable;
 
     #[tokio::test]
     async fn it_resizes_params_dynamically() {
-        let mut exe = Executable::from_quil("").with_config(ClientConfiguration::default());
+        let mut exe = Executable::from_quil("");
         let foo_len = |exe: &mut Executable<'_, '_>| exe.params.get("foo").unwrap().len();
 
         exe.with_parameter("foo", 0, 0.0);
@@ -760,35 +755,28 @@ mod describe_get_config {
     #[tokio::test]
     #[cfg(feature = "manual-tests")]
     async fn it_returns_cached_values() {
-        let mut exe = Executable::from_quil("");
         let config = ClientConfiguration::builder()
             .set_quilc_url(String::from("test"))
             .build()
             .unwrap();
-        exe.config = Some(config.clone());
-        let gotten = exe.get_config().await.unwrap_or_default();
-        assert_eq!(gotten.quilc_url(), config.quilc_url());
+        let client = Qcs::with_config(config.clone());
+        let mut exe = Executable::from_quil("").with_client(client);
+        let gotten = exe.get_client().await.unwrap_or_default();
+        assert_eq!(gotten.as_ref().get_config().quilc_url(), config.quilc_url());
     }
 }
 
 #[cfg(test)]
 #[cfg(feature = "manual-tests")]
 mod describe_qpu_for_id {
-    use std::sync::Arc;
-
-    use qcs_api_client_common::ClientConfiguration;
-
     use crate::compiler::quilc::CompilerOpts;
-    use crate::{
-        qpu::{self, Qcs},
-        Executable,
-    };
+    use crate::qpu;
+    use crate::{client::Qcs, Executable};
 
     #[tokio::test]
     async fn it_refreshes_auth_token() {
-        let mut exe = Executable::from_quil("");
         // Default config has no auth, so it should try to refresh
-        exe.config = Some(ClientConfiguration::default());
+        let mut exe = Executable::from_quil("").with_client(Qcs::default());
         let result = exe.qpu_for_id("blah").await;
         let Err(err) = result else {
             panic!("Expected an error!");
@@ -807,7 +795,7 @@ mod describe_qpu_for_id {
                 "".into(),
                 shots,
                 "Aspen-M-3".into(),
-                Arc::new(Qcs::with_config(exe.get_config().await.unwrap_or_default())),
+                exe.get_client().await.expect("should have client"),
                 exe.compile_with_quilc,
                 CompilerOpts::default(),
             )
@@ -815,7 +803,7 @@ mod describe_qpu_for_id {
             .unwrap(),
         );
         // Load config with no credentials to prevent creating a new Execution if it tries
-        exe.config = Some(ClientConfiguration::default());
+        let mut exe = exe.with_client(Qcs::default());
 
         assert!(exe.qpu_for_id("Aspen-M-3").await.is_ok());
     }
@@ -847,7 +835,7 @@ mod describe_qpu_for_id {
         // Cache so we can verify cache is not used.
         exe.qpu = Some(qpu);
         // Load config with no credentials to prevent creating the new Execution (which would fail anyway)
-        exe.config = Some(ClientConfiguration::default());
+        let mut exe = exe.with_client(Qcs::default());
         let result = exe.qpu_for_id("Aspen-8").await;
 
         assert!(matches!(result, Err(_)));
diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs
index 4241cefba..a6af42556 100644
--- a/crates/lib/src/lib.rs
+++ b/crates/lib/src/lib.rs
@@ -32,7 +32,6 @@
     semicolon_in_expressions_from_macros,
     trivial_casts,
     trivial_numeric_casts,
-    unaligned_references,
     unconditional_recursion,
     unreachable_pub,
     unused,
@@ -56,6 +55,7 @@ pub use execution_data::{
 };
 pub use register_data::RegisterData;
 
+pub mod client;
 pub mod compiler;
 mod executable;
 mod execution_data;
diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs
index 90b0f6014..f60897907 100644
--- a/crates/lib/src/qpu/api.rs
+++ b/crates/lib/src/qpu/api.rs
@@ -16,7 +16,7 @@ use qcs_api_client_grpc::{
 
 use crate::executable::Parameters;
 
-use super::client::{GrpcClientError, Qcs};
+use crate::client::{GrpcClientError, Qcs};
 
 pub(crate) fn params_into_job_execution_configuration(
     params: &Parameters,
diff --git a/crates/lib/src/qpu/execution.rs b/crates/lib/src/qpu/execution.rs
index 54289b2c6..38b830557 100644
--- a/crates/lib/src/qpu/execution.rs
+++ b/crates/lib/src/qpu/execution.rs
@@ -19,11 +19,11 @@ use crate::qpu::{rewrite_arithmetic, translation::translate};
 use crate::{ExecutionData, JobHandle};
 
 use super::api::{retrieve_results, submit, JobTarget};
-use super::client::{GrpcClientError, Qcs};
 use super::rewrite_arithmetic::RewrittenProgram;
 use super::translation::EncryptedTranslationResult;
 use super::QpuResultData;
 use super::{get_isa, GetIsaError};
+use crate::client::{GrpcClientError, Qcs};
 use crate::compiler::quilc::{self, CompilationResult, CompilerOpts, TargetDevice};
 
 /// Contains all the info needed for a single run of an [`crate::Executable`] against a QPU. Can be
diff --git a/crates/lib/src/qpu/mod.rs b/crates/lib/src/qpu/mod.rs
index 14e342f95..f048e6cb5 100644
--- a/crates/lib/src/qpu/mod.rs
+++ b/crates/lib/src/qpu/mod.rs
@@ -2,6 +2,7 @@
 //! the [`Execution`] struct in this module.
 use std::time::Duration;
 
+use crate::client::{OpenApiClientError, Qcs, DEFAULT_HTTP_API_TIMEOUT};
 use qcs_api_client_openapi::{
     apis::{
         quantum_processors_api::{
@@ -14,13 +15,11 @@ use qcs_api_client_openapi::{
 use tokio::time::error::Elapsed;
 
 pub mod api;
-pub mod client;
 mod execution;
 pub mod result_data;
 pub mod rewrite_arithmetic;
 pub mod translation;
 
-pub use client::Qcs;
 pub(crate) use execution::{Error as ExecutionError, Execution};
 #[allow(clippy::module_name_repetitions)]
 pub use result_data::{QpuResultData, ReadoutValues};
@@ -44,11 +43,11 @@ pub async fn get_isa(
 
     get_instruction_set_architecture(&client.get_openapi_client(), quantum_processor_id)
         .await
-        .map_err(client::OpenApiClientError::RequestFailed)
+        .map_err(OpenApiClientError::RequestFailed)
 }
 
 /// Error raised due to failure to get an ISA
-pub type GetIsaError = client::OpenApiClientError<GetInstructionSetArchitectureError>;
+pub type GetIsaError = OpenApiClientError<GetInstructionSetArchitectureError>;
 
 /// API Errors encountered when trying to list available quantum processors.
 #[derive(Debug, thiserror::Error)]
@@ -71,7 +70,7 @@ pub async fn list_quantum_processors(
     #[cfg(feature = "tracing")]
     tracing::debug!("listing quantum processors");
 
-    let timeout = timeout.unwrap_or(client::DEFAULT_HTTP_API_TIMEOUT);
+    let timeout = timeout.unwrap_or(DEFAULT_HTTP_API_TIMEOUT);
 
     tokio::time::timeout(timeout, async move {
         let mut quantum_processors = vec![];
diff --git a/crates/lib/src/qpu/translation.rs b/crates/lib/src/qpu/translation.rs
index e096bccb3..6e30de029 100644
--- a/crates/lib/src/qpu/translation.rs
+++ b/crates/lib/src/qpu/translation.rs
@@ -16,7 +16,7 @@ use qcs_api_client_openapi::{
 };
 use tokio::time::error::Elapsed;
 
-use super::client::{GrpcClientError, Qcs, DEFAULT_HTTP_API_TIMEOUT};
+use crate::client::{GrpcClientError, Qcs, DEFAULT_HTTP_API_TIMEOUT};
 
 /// An encrypted and translated program, along with `readout_map`
 /// to map job `readout_data` back to program-declared variables.
diff --git a/crates/lib/src/qvm/execution.rs b/crates/lib/src/qvm/execution.rs
index 476e96b36..cbeb6f877 100644
--- a/crates/lib/src/qvm/execution.rs
+++ b/crates/lib/src/qvm/execution.rs
@@ -1,13 +1,13 @@
 use std::borrow::Cow;
 use std::str::FromStr;
 
-use qcs_api_client_common::ClientConfiguration;
 use quil_rs::{
     instruction::{ArithmeticOperand, Instruction, MemoryReference, Move},
     program::ProgramError,
     Program,
 };
 
+use crate::client::Qcs;
 use crate::executable::Parameters;
 
 use super::{QvmResultData, Request, Response};
@@ -59,7 +59,7 @@ impl Execution {
         shots: u16,
         readouts: &[Cow<'_, str>],
         params: &Parameters,
-        config: &ClientConfiguration,
+        client: &Qcs,
     ) -> Result<QvmResultData, Error> {
         #[cfg(feature = "tracing")]
         tracing::debug!(
@@ -105,7 +105,7 @@ impl Execution {
                 instruction_count += 1;
             }
         }
-        let result = self.execute(shots, readouts, config).await;
+        let result = self.execute(shots, readouts, client).await;
         for _ in 0..instruction_count {
             self.program.instructions.remove(0);
         }
@@ -116,7 +116,7 @@ impl Execution {
         &self,
         shots: u16,
         readouts: &[Cow<'_, str>],
-        config: &ClientConfiguration,
+        client: &Qcs,
     ) -> Result<QvmResultData, Error> {
         #[cfg(feature = "tracing")]
         tracing::debug!(
@@ -126,21 +126,22 @@ impl Execution {
         );
 
         let request = Request::new(&self.program.to_string(true), shots, readouts);
+        let qvm_url = client.get_config().qvm_url();
 
         let client = reqwest::Client::new();
         let response = client
-            .post(config.qvm_url())
+            .post(qvm_url)
             .json(&request)
             .send()
             .await
             .map_err(|source| Error::QvmCommunication {
-                qvm_url: config.qvm_url().into(),
+                qvm_url: qvm_url.to_string(),
                 source,
             })?;
 
         match response.json::<Response>().await {
             Err(source) => Err(Error::QvmCommunication {
-                qvm_url: config.qvm_url().into(),
+                qvm_url: qvm_url.to_string(),
                 source,
             }),
             Ok(Response::Success(response)) => {
@@ -179,7 +180,9 @@ pub(crate) enum Error {
 
 #[cfg(test)]
 mod describe_execution {
-    use super::{ClientConfiguration, Execution, Parameters};
+    use crate::client::Qcs;
+
+    use super::{Execution, Parameters};
 
     #[tokio::test]
     async fn it_errs_on_excess_parameters() {
@@ -188,9 +191,7 @@ mod describe_execution {
         let mut params = Parameters::new();
         params.insert("doesnt_exist".into(), vec![0.0]);
 
-        let result = exe
-            .run(1, &[], &params, &ClientConfiguration::default())
-            .await;
+        let result = exe.run(1, &[], &params, &Qcs::default()).await;
         if let Err(e) = result {
             assert!(e.to_string().contains("doesnt_exist"));
         } else {
@@ -205,9 +206,7 @@ mod describe_execution {
         let mut params = Parameters::new();
         params.insert("ro".into(), vec![0.0]);
 
-        let result = exe
-            .run(1, &[], &params, &ClientConfiguration::default())
-            .await;
+        let result = exe.run(1, &[], &params, &Qcs::default()).await;
         if let Err(e) = result {
             let err_string = e.to_string();
             assert!(err_string.contains("ro"));
diff --git a/crates/python/qcs_sdk/__init__.pyi b/crates/python/qcs_sdk/__init__.pyi
index 7b96a51b6..927b82803 100644
--- a/crates/python/qcs_sdk/__init__.pyi
+++ b/crates/python/qcs_sdk/__init__.pyi
@@ -23,7 +23,7 @@ from ._register_data import (
     RegisterData as RegisterData,
 )
 
-from .qpu.client import (
+from .client import (
     # classes
     QCSClient as QCSClient,
 )
diff --git a/crates/python/qcs_sdk/qpu/client.pyi b/crates/python/qcs_sdk/client.pyi
similarity index 95%
rename from crates/python/qcs_sdk/qpu/client.pyi
rename to crates/python/qcs_sdk/client.pyi
index adf5c9853..eb0e65e1e 100644
--- a/crates/python/qcs_sdk/qpu/client.pyi
+++ b/crates/python/qcs_sdk/client.pyi
@@ -2,13 +2,13 @@ from typing import Optional, final
 
 class LoadClientError(RuntimeError):
     """Error encountered while loading the QCS API client configuration from the environment configuration."""
-    ...
 
+    ...
 
 class BuildClientError(RuntimeError):
     """Error encountered while building the QCS API client configuration manually."""
-    ...
 
+    ...
 
 @final
 class QCSClient:
@@ -31,7 +31,6 @@ class QCSClient:
         Prefer to use ``QCSClient.load`` to construct an environment-based profile.
         """
         ...
-
     @staticmethod
     def load(
         profile_name: Optional[str] = None,
@@ -48,7 +47,6 @@ class QCSClient:
         See for details: https://docs.rigetti.com/qcs/references/qcs-client-configuration#environment-variables-and-configuration-files
         """
         ...
-
     @staticmethod
     async def load_async(
         profile_name: Optional[str] = None,
@@ -66,22 +64,18 @@ class QCSClient:
         See for details: https://docs.rigetti.com/qcs/references/qcs-client-configuration#environment-variables-and-configuration-files
         """
         ...
-
     @property
     def api_url(self) -> str:
         """URL to access the QCS API."""
         ...
-
     @property
     def grpc_api_url(self) -> str:
         """URL to access the gRPC API."""
         ...
-
     @property
     def quilc_url(self) -> str:
         """URL to access the `quilc` compiler."""
         ...
-
     @property
     def qvm_url(self) -> str:
         """URL to access the QVM."""
@@ -91,11 +85,7 @@ class QCSClient:
 class QCSClientAuthServer:
     """Authentication server configuration for the QCS API."""
 
-    def __new__(
-        cls,
-        client_id: str,
-        issuer: str
-    ) -> "QCSClientAuthServer":
+    def __new__(cls, client_id: str, issuer: str) -> "QCSClientAuthServer":
         """
         Manually define authentication server parameters.
 
@@ -103,9 +93,8 @@ class QCSClientAuthServer:
         :param issuer: The OAuth token issuer url. If ``None``, a default value is used.
         """
         ...
-
     @property
-    def client_id(self) -> str:...
+    def client_id(self) -> str: ...
     @client_id.setter
     def client_id(self, value: str): ...
     @property
@@ -113,7 +102,6 @@ class QCSClientAuthServer:
     @issuer.setter
     def issuer(self, value: str): ...
 
-
 @final
 class QCSClientTokens:
     """Authentication tokens for the QCS API."""
@@ -122,7 +110,7 @@ class QCSClientTokens:
         cls,
         bearer_access_token: str,
         refresh_token: str,
-    ) -> "QCSClientTokens": 
+    ) -> "QCSClientTokens":
         """
         Manually define authentication session tokens.
 
@@ -130,7 +118,6 @@ class QCSClientTokens:
         :param refresh_token: A credential to refresh the bearer_access_token when it expires.
         """
         ...
-
     @property
     def bearer_access_token(self) -> Optional[str]: ...
     @bearer_access_token.setter
diff --git a/crates/python/qcs_sdk/compiler/quilc.pyi b/crates/python/qcs_sdk/compiler/quilc.pyi
index 2efeb22a0..939ae79f2 100644
--- a/crates/python/qcs_sdk/compiler/quilc.pyi
+++ b/crates/python/qcs_sdk/compiler/quilc.pyi
@@ -1,7 +1,7 @@
 from typing import List, Optional, final
 
 from ..qpu.isa import InstructionSetArchitecture
-from ..qpu.client import QCSClient
+from qcs_sdk.client import QCSClient
 
 DEFAULT_COMPILER_TIMEOUT: float
 """Number of seconds to wait before timing out."""
diff --git a/crates/python/qcs_sdk/qpu/__init__.pyi b/crates/python/qcs_sdk/qpu/__init__.pyi
index 3773ba716..10eadf362 100644
--- a/crates/python/qcs_sdk/qpu/__init__.pyi
+++ b/crates/python/qcs_sdk/qpu/__init__.pyi
@@ -1,16 +1,15 @@
 from typing import List, Optional
 
-from .client import QCSClient
+from qcs_sdk.client import QCSClient
 from ._result_data import (
     QPUResultData as QPUResultData,
     ReadoutValues as ReadoutValues,
 )
 
-
 class ListQuantumProcessorsError(RuntimeError):
     """A request to list available Quantum Processors failed."""
-    ...
 
+    ...
 
 def list_quantum_processors(
     client: Optional[QCSClient] = None,
@@ -26,7 +25,6 @@ def list_quantum_processors(
     """
     ...
 
-
 async def list_quantum_processors_async(
     client: Optional[QCSClient] = None,
     timeout: Optional[float] = None,
@@ -40,4 +38,4 @@ async def list_quantum_processors_async(
 
     :raises ListQuantumProcessorsError: If the request to list available QPU IDs failed.
     """
-    ...
\ No newline at end of file
+    ...
diff --git a/crates/python/qcs_sdk/qpu/api.pyi b/crates/python/qcs_sdk/qpu/api.pyi
index d75b5190e..eee4f7d4e 100644
--- a/crates/python/qcs_sdk/qpu/api.pyi
+++ b/crates/python/qcs_sdk/qpu/api.pyi
@@ -1,17 +1,16 @@
 from typing import Dict, List, Optional, final
 
-from .client import QCSClient
-
+from qcs_sdk.client import QCSClient
 
 class SubmissionError(RuntimeError):
     """There was a problem submitting the program to QCS for execution."""
-    ...
 
+    ...
 
 class RetrieveResultsError(RuntimeError):
     """There was a problem retrieving program execution results from QCS."""
-    ...
 
+    ...
 
 @final
 class Register:
@@ -32,19 +31,15 @@ class Register:
 
     def is_i32(self) -> bool: ...
     def is_complex32(self) -> bool: ...
-
     def as_i32(self) -> Optional[List[int]]: ...
     def as_complex32(self) -> Optional[List[complex]]: ...
-
     def to_i32(self) -> List[int]: ...
     def to_complex32(self) -> List[complex]: ...
-
     @staticmethod
     def from_i32(inner: List[int]) -> "Register": ...
     @staticmethod
     def from_complex32(inner: List[complex]) -> "Register": ...
 
-
 @final
 class ExecutionResult:
     """Execution readout data from a particular memory location."""
@@ -53,18 +48,15 @@ class ExecutionResult:
     def shape(self) -> List[int]:
         """The shape of the result data."""
         ...
-
     @property
     def data(self) -> Register:
         """The result data for all shots by the particular memory location."""
         ...
-
     @property
     def dtype(self) -> str:
         """The type of the result data (as a `numpy` `dtype`)."""
         ...
 
-
 @final
 class ExecutionResults:
     """Execution readout data for all memory locations."""
@@ -77,13 +69,11 @@ class ExecutionResults:
         See `TranslationResult.ro_sources` which provides the mapping from the filter node name to the name of the memory declaration in the source program.
         """
         ...
-
     @property
     def execution_duration_microseconds(self) -> Optional[int]:
         """The time spent executing the program."""
         ...
 
-
 def submit(
     program: str,
     patch_values: Dict[str, List[float]],
@@ -107,7 +97,6 @@ def submit(
     """
     ...
 
-
 async def submit_async(
     program: str,
     patch_values: Dict[str, List[float]],
@@ -132,7 +121,6 @@ async def submit_async(
     """
     ...
 
-
 def retrieve_results(
     job_id: str,
     quantum_processor_id: str,
@@ -154,7 +142,6 @@ def retrieve_results(
     """
     ...
 
-
 async def retrieve_results_async(
     job_id: str,
     quantum_processor_id: str,
diff --git a/crates/python/qcs_sdk/qpu/isa.pyi b/crates/python/qcs_sdk/qpu/isa.pyi
index 91f80ddb4..3210cfe93 100644
--- a/crates/python/qcs_sdk/qpu/isa.pyi
+++ b/crates/python/qcs_sdk/qpu/isa.pyi
@@ -1,17 +1,17 @@
 from enum import Enum
 from typing import List, Optional, final
 
-from .client import QCSClient
+from qcs_sdk.client import QCSClient
 
 class SerializeISAError(ValueError):
     """If an ``InstructionSetArchitecture`` could not be serialized or deserialized correctly."""
-    ...
 
+    ...
 
 class GetISAError(RuntimeError):
     """If there was an issue fetching ``InstructionSetArchitecture`` from the QCS API."""
-    ...
 
+    ...
 
 @final
 class Family(Enum):
@@ -21,11 +21,10 @@ class Family(Enum):
     Value "Full"  implies that each node is connected to every other (fully-connected architecture).
     """
 
-    NONE = "NONE",
-    Full = "Full",
-    Aspen = "Aspen",
-    Ankaa = "Ankaa",
-
+    NONE = ("NONE",)
+    Full = ("Full",)
+    Aspen = ("Aspen",)
+    Ankaa = ("Ankaa",)
 
 @final
 class Node:
@@ -47,7 +46,6 @@ class Node:
     @node_id.setter
     def node_id(self, value: int): ...
 
-
 @final
 class Edge:
     """
@@ -71,7 +69,6 @@ class Edge:
     @node_ids.setter
     def node_ids(self, value: List[int]): ...
 
-
 @final
 class Parameter:
     """A parameter to an operation."""
@@ -83,7 +80,6 @@ class Parameter:
     @name.setter
     def name(self, value: str): ...
 
-
 @final
 class Characteristic:
     """A measured characteristic of an operation."""
@@ -94,21 +90,18 @@ class Characteristic:
         ...
     @name.setter
     def name(self, value: str): ...
-
     @property
     def value(self) -> float:
         """The characteristic value measured."""
         ...
     @value.setter
     def value(self, value: float): ...
-
     @property
     def error(self) -> Optional[float]:
         """The error in the characteristic value, or None if otherwise."""
         ...
     @error.setter
     def error(self, value: Optional[float]): ...
-
     @property
     def node_ids(self) -> Optional[List[int]]:
         """
@@ -120,14 +113,12 @@ class Characteristic:
         ...
     @node_ids.setter
     def node_ids(self, value: Optional[List[int]]): ...
-
     @property
     def timestamp(self) -> str:
         """ISO8601 date and time at which the characteristic was measured."""
         ...
     @timestamp.setter
     def timestamp(self, value: str): ...
-
     @property
     def parameter_values(self) -> Optional[List[float]]:
         """
@@ -139,7 +130,6 @@ class Characteristic:
     @parameter_values.setter
     def parameter_values(self, value: Optional[List[float]]): ...
 
-
 @final
 class OperationSite:
     """A site for an operation, with its site-dependent characteristics."""
@@ -153,7 +143,6 @@ class OperationSite:
         ...
     @node_ids.setter
     def node_ids(self, value: List[int]): ...
-
     @property
     def characteristics(self) -> List[Characteristic]:
         """The list of site-dependent characteristics of this operation."""
@@ -161,7 +150,6 @@ class OperationSite:
     @characteristics.setter
     def characteristics(self, value: List[Characteristic]): ...
 
-
 @final
 class Operation:
     """An operation, with its sites and site-independent characteristics."""
@@ -172,21 +160,18 @@ class Operation:
         ...
     @name.setter
     def name(self, value: str): ...
-
     @property
     def node_count(self) -> Optional[int]:
         """The number of nodes that this operation applies to. None if unspecified."""
         ...
     @node_count.setter
     def node_count(self, value: Optional[int]): ...
-
     @property
     def parameters(self) -> List[Parameter]:
         """The list of parameters. Each parameter must be uniquely named. May be empty."""
         ...
     @parameters.setter
     def parameters(self, value: List[Parameter]): ...
-
     @property
     def sites(self) -> List[OperationSite]:
         """
@@ -196,7 +181,6 @@ class Operation:
         ...
     @sites.setter
     def sites(self, value: List[OperationSite]): ...
-
     @property
     def characteristics(self) -> List[Characteristic]:
         """The list of site-independent characteristics of this operation."""
@@ -204,7 +188,6 @@ class Operation:
     @characteristics.setter
     def characteristics(self, value: List[Characteristic]): ...
 
-
 @final
 class Architecture:
     """
@@ -229,7 +212,7 @@ class Architecture:
     Edges are ordered top-left to bottom-right in this orientation as well, with horizontal rows
     alternating with vertical rows. Ankaa chips are, in vertical orientation,
     7 qubits wide and 12 tall. This architecture may also be presented in "landscape"
-    orientation, which is a simple 90-degree clockwise rotation of the vertical orientation. 
+    orientation, which is a simple 90-degree clockwise rotation of the vertical orientation.
 
     Note that the operations that are actually available are defined entirely by ``Operation``
     instances. The presence of a node or edge in the ``Architecture`` model provides no guarantee
@@ -242,14 +225,12 @@ class Architecture:
         ...
     @family.setter
     def family(self, value: Family): ...
-
     @property
     def nodes(self) -> List[Node]:
         """A list of all computational nodes in the instruction set architecture."""
         ...
     @nodes.setter
     def nodes(self, value: List[Node]): ...
-
     @property
     def edges(self) -> List[Edge]:
         """A list of all computational edges in the instruction set architecture."""
@@ -257,8 +238,6 @@ class Architecture:
     @edges.setter
     def edges(self, value: List[Edge]): ...
 
-
-
 @final
 class InstructionSetArchitecture:
     """
@@ -278,39 +257,34 @@ class InstructionSetArchitecture:
         ...
     @name.setter
     def name(self, value: str): ...
-
     @property
     def architecture(self) -> Architecture:
         """The architecture of the quantum processor."""
         ...
     @architecture.setter
     def architecture(self, value: Architecture): ...
-
     @property
     def instructions(self) -> List[Operation]:
         """The list of native QUIL instructions supported by the quantum processor."""
         ...
     @instructions.setter
     def instructions(self, value: List[Operation]): ...
-
     @property
     def benchmarks(self) -> List[Operation]:
         """The list of benchmarks that have characterized the quantum processor."""
         ...
     @benchmarks.setter
     def benchmarks(self, value: List[Operation]): ...
-
     @staticmethod
     def from_raw(value: str) -> "InstructionSetArchitecture":
         """
         Deserialize an ``InstructionSetArchitecture`` from a json representation.
 
         :param value: The json-serialized ``InstructionSetArchitecture`` to deserialize.
-        
+
         :raises SerializeISAError: If the input string was not deserialized correctly.
         """
         ...
-    
     def json(self, pretty: bool = False) -> str:
         """
         Serialize the ``InstructionSetArchitecture`` to a json string, optionally pretty-printed.
@@ -321,7 +295,6 @@ class InstructionSetArchitecture:
         """
         ...
 
-
 def get_instruction_set_architecture(
     quantum_processor_id: str,
     client: Optional[QCSClient] = ...,
@@ -337,7 +310,6 @@ def get_instruction_set_architecture(
     """
     ...
 
-
 async def get_instruction_set_architecture_async(
     quantum_processor_id: str,
     client: QCSClient = ...,
diff --git a/crates/python/qcs_sdk/qpu/translation.pyi b/crates/python/qcs_sdk/qpu/translation.pyi
index 2007464e5..ea8fd13da 100644
--- a/crates/python/qcs_sdk/qpu/translation.pyi
+++ b/crates/python/qcs_sdk/qpu/translation.pyi
@@ -1,17 +1,16 @@
 from typing import Dict, Optional, final
 
-from .client import QCSClient
-
+from qcs_sdk.client import QCSClient
 
 class GetQuiltCalibrationsError(RuntimeError):
     """An error occured while fetching Quil-T calibrations."""
-    ...
 
+    ...
 
 class TranslationError(RuntimeError):
     """An error occured while translating a program."""
-    ...
 
+    ...
 
 @final
 class QuiltCalibrations:
@@ -23,7 +22,6 @@ class QuiltCalibrations:
         ...
     @quilt.setter
     def quilt(self, value: str): ...
-
     @property
     def settings_timestamp(self) -> Optional[str]:
         """ISO8601 timestamp of the settings used to generate these calibrations."""
@@ -31,7 +29,6 @@ class QuiltCalibrations:
     @settings_timestamp.setter
     def settings_timestamp(self, value: Optional[str]): ...
 
-
 @final
 class TranslationResult:
     """
@@ -42,13 +39,11 @@ class TranslationResult:
     def program(self) -> str:
         """The translated programs."""
         ...
-
     @property
     def ro_sources(self) -> Optional[Dict[str, str]]:
         """A mapping from the program's memory references to the key used to index the results map."""
         ...
 
-
 def get_quilt_calibrations(
     quantum_processor_id: str,
     client: Optional[QCSClient] = None,
@@ -68,7 +63,6 @@ def get_quilt_calibrations(
     """
     ...
 
-
 async def get_quilt_calibrations_async(
     quantum_processor_id: str,
     client: Optional[QCSClient] = None,
@@ -89,7 +83,6 @@ async def get_quilt_calibrations_async(
     """
     ...
 
-
 def translate(
     native_quil: str,
     num_shots: int,
@@ -111,7 +104,6 @@ def translate(
     """
     ...
 
-
 async def translate_async(
     native_quil: str,
     num_shots: int,
@@ -132,4 +124,4 @@ async def translate_async(
     :raises LoadClientError: If there is an issue loading the QCS Client configuration.
     :raises TranslationError: If the `native_quil` program could not be translated.
     """
-    ...
\ No newline at end of file
+    ...
diff --git a/crates/python/src/qpu/client.rs b/crates/python/src/client.rs
similarity index 88%
rename from crates/python/src/qpu/client.rs
rename to crates/python/src/client.rs
index c6d0b63ed..018a5e4df 100644
--- a/crates/python/src/qpu/client.rs
+++ b/crates/python/src/client.rs
@@ -1,6 +1,5 @@
-use qcs_api_client_common::{
-    configuration::{AuthServer, BuildError, ClientConfigurationBuilder, Tokens},
-    ClientConfiguration,
+use qcs_api_client_common::configuration::{
+    AuthServer, BuildError, ClientConfigurationBuilder, Tokens,
 };
 use rigetti_pyo3::{
     create_init_submodule, py_wrap_data_struct, py_wrap_error, py_wrap_type,
@@ -11,7 +10,7 @@ use rigetti_pyo3::{
     wrap_error, ToPythonError,
 };
 
-use qcs::qpu::{client, Qcs};
+use qcs::client::{self, Qcs};
 
 use crate::py_sync::{py_async, py_sync};
 
@@ -96,27 +95,22 @@ py_wrap_type! {
 }
 
 impl PyQcsClient {
-    pub(crate) async fn get_or_create_client(client: Option<Self>) -> PyResult<Qcs> {
-        Ok(match client {
+    pub(crate) async fn get_or_create_client(client: Option<Self>) -> Qcs {
+        match client {
             Some(client) => client.into(),
-            None => Qcs::load()
-                .await
-                .map_err(RustLoadClientError::from)
-                .map_err(RustLoadClientError::to_py_err)?,
-        })
+            None => Qcs::load().await,
+        }
     }
 
     async fn load(profile_name: Option<String>, use_gateway: Option<bool>) -> PyResult<Self> {
-        let config = match profile_name {
-            Some(profile_name) => ClientConfiguration::load_profile(profile_name).await,
-            None => ClientConfiguration::load_default().await,
+        let client = match profile_name {
+            Some(profile_name) => Qcs::with_profile(profile_name)
+                .await
+                .map_err(RustLoadClientError)
+                .map_err(RustLoadClientError::to_py_err)?,
+            None => Qcs::load().await,
         };
 
-        let client = config
-            .map(Qcs::with_config)
-            .map_err(RustLoadClientError)
-            .map_err(RustLoadClientError::to_py_err)?;
-
         let client = match use_gateway {
             None => client,
             Some(use_gateway) => client.with_use_gateway(use_gateway),
@@ -183,7 +177,11 @@ impl PyQcsClient {
     #[staticmethod]
     #[args("/", profile_name = "None", use_gateway = "None")]
     #[pyo3(name = "load")]
-    pub fn py_load(py: Python<'_>, profile_name: Option<String>, use_gateway: Option<bool>) -> PyResult<Self> {
+    pub fn py_load(
+        py: Python<'_>,
+        profile_name: Option<String>,
+        use_gateway: Option<bool>,
+    ) -> PyResult<Self> {
         py_sync!(py, Self::load(profile_name, use_gateway))
     }
 
diff --git a/crates/python/src/compiler/quilc.rs b/crates/python/src/compiler/quilc.rs
index 6c4b605f5..a9ddd0a35 100644
--- a/crates/python/src/compiler/quilc.rs
+++ b/crates/python/src/compiler/quilc.rs
@@ -16,8 +16,8 @@ use rigetti_pyo3::{
     wrap_error, PyWrapper, ToPythonError,
 };
 
+use crate::client::PyQcsClient;
 use crate::py_sync::py_function_sync_async;
-use crate::qpu::client::PyQcsClient;
 use crate::qpu::isa::PyInstructionSetArchitecture;
 
 create_init_submodule! {
@@ -107,7 +107,7 @@ py_function_sync_async! {
         client: Option<PyQcsClient>,
         options: Option<PyCompilerOpts>,
     ) -> PyResult<PyCompilationResult> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
         let options = options.unwrap_or_default();
         qcs::compiler::quilc::compile_program(&quil, target.into(), &client, options.into())
             .map_err(RustQuilcError::from)
@@ -190,7 +190,7 @@ py_function_sync_async! {
     async fn get_version_info(
         client: Option<PyQcsClient>,
     ) -> PyResult<String> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
         qcs::compiler::quilc::get_version_info(&client)
             .map_err(RustQuilcError::from)
             .map_err(RustQuilcError::to_py_err)
@@ -243,7 +243,7 @@ py_function_sync_async! {
         request: PyConjugateByCliffordRequest,
         client: Option<PyQcsClient>,
     ) -> PyResult<PyConjugatePauliByCliffordResponse> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
         qcs::compiler::quilc::conjugate_pauli_by_clifford(&client, request.into())
             .map(PyConjugatePauliByCliffordResponse::from)
             .map_err(RustQuilcError::from)
@@ -293,7 +293,7 @@ py_function_sync_async! {
         request: PyRandomizedBenchmarkingRequest,
         client: Option<PyQcsClient>,
     ) -> PyResult<PyGenerateRandomizedBenchmarkingSequenceResponse> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
         qcs::compiler::quilc::generate_randomized_benchmarking_sequence(&client, request.into())
             .map(PyGenerateRandomizedBenchmarkingSequenceResponse::from)
             .map_err(RustQuilcError::from)
diff --git a/crates/python/src/executable.rs b/crates/python/src/executable.rs
index fa7ce71d9..457685ea9 100644
--- a/crates/python/src/executable.rs
+++ b/crates/python/src/executable.rs
@@ -154,19 +154,25 @@ impl PyExecutable {
         let translation_options =
             Option::<TranslationOptions>::py_try_from(py, &translation_options)?;
         match endpoint_id {
-            Some(endpoint_id) => py_sync!(py, py_executable_data!(
-                self,
-                execute_on_qpu_with_endpoint,
-                quantum_processor_id,
-                endpoint_id,
-                translation_options,
-            )),
-            None => py_sync!(py, py_executable_data!(
-                self,
-                execute_on_qpu,
-                quantum_processor_id,
-                translation_options,
-            )),
+            Some(endpoint_id) => py_sync!(
+                py,
+                py_executable_data!(
+                    self,
+                    execute_on_qpu_with_endpoint,
+                    quantum_processor_id,
+                    endpoint_id,
+                    translation_options,
+                )
+            ),
+            None => py_sync!(
+                py,
+                py_executable_data!(
+                    self,
+                    execute_on_qpu,
+                    quantum_processor_id,
+                    translation_options,
+                )
+            ),
         }
     }
 
@@ -214,19 +220,25 @@ impl PyExecutable {
         let translation_options =
             Option::<TranslationOptions>::py_try_from(py, &translation_options)?;
         match endpoint_id {
-            Some(endpoint_id) => py_sync!(py, py_job_handle!(
-                self,
-                submit_to_qpu_with_endpoint,
-                quantum_processor_id,
-                endpoint_id,
-                translation_options,
-            )),
-            None => py_sync!(py, py_job_handle!(
-                self,
-                submit_to_qpu,
-                quantum_processor_id,
-                translation_options
-            )),
+            Some(endpoint_id) => py_sync!(
+                py,
+                py_job_handle!(
+                    self,
+                    submit_to_qpu_with_endpoint,
+                    quantum_processor_id,
+                    endpoint_id,
+                    translation_options,
+                )
+            ),
+            None => py_sync!(
+                py,
+                py_job_handle!(
+                    self,
+                    submit_to_qpu,
+                    quantum_processor_id,
+                    translation_options
+                )
+            ),
         }
     }
 
@@ -265,12 +277,15 @@ impl PyExecutable {
         }
     }
 
-    pub fn retrieve_results(&mut self, py: Python<'_>, job_handle: PyJobHandle) -> PyResult<PyExecutionData> {
-        py_sync!(py, py_executable_data!(
-            self,
-            retrieve_results,
-            job_handle.into()
-        ))
+    pub fn retrieve_results(
+        &mut self,
+        py: Python<'_>,
+        job_handle: PyJobHandle,
+    ) -> PyResult<PyExecutionData> {
+        py_sync!(
+            py,
+            py_executable_data!(self, retrieve_results, job_handle.into())
+        )
     }
 
     pub fn retrieve_results_async<'py>(
diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs
index 1ab12c727..f7b49c02e 100644
--- a/crates/python/src/lib.rs
+++ b/crates/python/src/lib.rs
@@ -4,6 +4,7 @@ use rigetti_pyo3::create_init_submodule;
 use executable::ExecutionError;
 use execution_data::RegisterMatrixConversionError;
 
+pub mod client;
 pub mod compiler;
 pub mod executable;
 pub mod execution_data;
@@ -25,13 +26,14 @@ create_init_submodule! {
         executable::PyJobHandle,
         executable::PyService,
         register_data::PyRegisterData,
-        qpu::client::PyQcsClient
+        client::PyQcsClient
     ],
     errors: [
         ExecutionError,
         RegisterMatrixConversionError
     ],
     submodules: [
+        "client": client::init_submodule,
         "compiler": compiler::init_submodule,
         "qpu": qpu::init_submodule,
         "qvm": qvm::init_submodule
diff --git a/crates/python/src/qpu/api.rs b/crates/python/src/qpu/api.rs
index 28c10ae4f..3b7bb38f9 100644
--- a/crates/python/src/qpu/api.rs
+++ b/crates/python/src/qpu/api.rs
@@ -8,7 +8,8 @@ use pyo3::{
     types::{PyComplex, PyInt},
     Py, PyResult,
 };
-use qcs::qpu::{api::JobTarget, client::GrpcClientError};
+use qcs::client::GrpcClientError;
+use qcs::qpu::api::JobTarget;
 use qcs_api_client_grpc::models::controller::{readout_values, ControllerJobExecutionResult};
 use rigetti_pyo3::{
     create_init_submodule, num_complex, py_wrap_error, py_wrap_union_enum, wrap_error,
@@ -17,7 +18,7 @@ use rigetti_pyo3::{
 
 use crate::py_sync::py_function_sync_async;
 
-use super::client::PyQcsClient;
+use crate::client::PyQcsClient;
 
 create_init_submodule! {
     classes: [
@@ -69,7 +70,7 @@ py_function_sync_async! {
         client: Option<PyQcsClient>,
         endpoint_id: Option<String>,
     ) -> PyResult<String> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
 
         // Is there a better way to map these patch_values keys? This
         // negates the whole purpose of [`submit`] using `Box<str>`,
@@ -189,7 +190,7 @@ py_function_sync_async! {
         client: Option<PyQcsClient>,
         endpoint_id: Option<String>,
     ) -> PyResult<ExecutionResults> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
 
         let job_target = endpoint_id.map_or_else(|| JobTarget::QuantumProcessorId(quantum_processor_id), JobTarget::EndpointId);
 
diff --git a/crates/python/src/qpu/isa.rs b/crates/python/src/qpu/isa.rs
index a11fd43b1..9b6f501c5 100644
--- a/crates/python/src/qpu/isa.rs
+++ b/crates/python/src/qpu/isa.rs
@@ -15,7 +15,7 @@ use rigetti_pyo3::{
 
 use qcs::qpu::get_isa;
 
-use crate::{py_sync::py_function_sync_async, qpu::client::PyQcsClient};
+use crate::{client::PyQcsClient, py_sync::py_function_sync_async};
 
 create_init_submodule! {
     classes: [
@@ -148,7 +148,7 @@ py_function_sync_async! {
         quantum_processor_id: String,
         client: Option<PyQcsClient>,
     ) -> PyResult<PyInstructionSetArchitecture> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
 
         get_isa(&quantum_processor_id, &client)
             .await
diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs
index e1773db1a..283481dce 100644
--- a/crates/python/src/qpu/mod.rs
+++ b/crates/python/src/qpu/mod.rs
@@ -6,12 +6,12 @@ use rigetti_pyo3::{create_init_submodule, py_wrap_error, wrap_error, ToPythonErr
 pub use result_data::{PyQpuResultData, PyReadoutValues};
 
 pub mod api;
-pub mod client;
 pub mod isa;
 mod result_data;
 pub mod rewrite_arithmetic;
 pub mod translation;
 
+use crate::client::PyQcsClient;
 use crate::py_sync::py_function_sync_async;
 
 create_init_submodule! {
@@ -28,7 +28,6 @@ create_init_submodule! {
     ],
     submodules: [
         "api": api::init_submodule,
-        "client": client::init_submodule,
         "isa": isa::init_submodule,
         "rewrite_arithmetic": rewrite_arithmetic::init_submodule,
         "translation": translation::init_submodule
@@ -48,10 +47,10 @@ py_wrap_error!(
 py_function_sync_async! {
     #[pyfunction(client = "None", timeout = "None")]
     async fn list_quantum_processors(
-        client: Option<client::PyQcsClient>,
+        client: Option<PyQcsClient>,
         timeout: Option<f64>,
     ) -> PyResult<Vec<String>> {
-        let client = client::PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
         let timeout = timeout.map(Duration::from_secs_f64);
         qcs::qpu::list_quantum_processors(&client, timeout)
             .await
diff --git a/crates/python/src/qpu/translation.rs b/crates/python/src/qpu/translation.rs
index 9bfdb01b2..1e5c2a15b 100644
--- a/crates/python/src/qpu/translation.rs
+++ b/crates/python/src/qpu/translation.rs
@@ -4,7 +4,7 @@ use std::{collections::HashMap, time::Duration};
 use pyo3::{
     exceptions::PyRuntimeError, pyclass, pyfunction, types::PyString, Py, PyResult, Python,
 };
-use qcs::qpu::client::GrpcClientError;
+use qcs::client::GrpcClientError;
 use qcs_api_client_grpc::services::translation::TranslationOptions;
 use qcs_api_client_openapi::models::GetQuiltCalibrationsResponse;
 use rigetti_pyo3::{
@@ -13,7 +13,7 @@ use rigetti_pyo3::{
 
 use crate::{grpc::models::translation::PyTranslationOptions, py_sync::py_function_sync_async};
 
-use super::client::PyQcsClient;
+use crate::client::PyQcsClient;
 
 create_init_submodule! {
     classes: [
@@ -58,7 +58,7 @@ py_function_sync_async! {
         client: Option<PyQcsClient>,
         timeout: Option<f64>,
     ) -> PyResult<PyQuiltCalibrations> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
         let timeout = timeout.map(Duration::from_secs_f64);
         qcs::qpu::translation::get_quilt_calibrations(&quantum_processor_id, &client, timeout)
             .await
@@ -114,7 +114,7 @@ py_function_sync_async! {
         client: Option<PyQcsClient>,
         translation_options: Option<PyTranslationOptions>,
     ) -> PyResult<PyTranslationResult> {
-        let client = PyQcsClient::get_or_create_client(client).await?;
+        let client = PyQcsClient::get_or_create_client(client).await;
         let translation_options = Python::with_gil(|py| {
             Option::<TranslationOptions>::py_try_from(py, &translation_options)
         })?;


Test patch:
diff --git a/crates/lib/tests/basic_qvm.rs b/crates/lib/tests/basic_qvm.rs
index dad161dd3..5b1b39409 100644
--- a/crates/lib/tests/basic_qvm.rs
+++ b/crates/lib/tests/basic_qvm.rs
@@ -1,8 +1,7 @@
 //! These are the integration tests for [`qcs::Executable::execute_on_qvm`].
 //! In order to run them, QVM's web server must be running at localhost:5000.
 
-use qcs::Executable;
-use qcs_api_client_openapi::common::ClientConfiguration;
+use qcs::{client::Qcs, Executable};
 
 const PROGRAM: &str = r##"
 DECLARE first BIT
@@ -20,7 +19,7 @@ async fn test_bell_state() {
     const SHOTS: u16 = 10;
 
     let data = Executable::from_quil(PROGRAM)
-        .with_config(ClientConfiguration::default())
+        .with_client(Qcs::load().await)
         .with_shots(SHOTS)
         .read_from("first")
         .read_from("second")
diff --git a/crates/lib/tests/parametric_compilation.rs b/crates/lib/tests/parametric_compilation.rs
index a5917d987..66ad8d877 100644
--- a/crates/lib/tests/parametric_compilation.rs
+++ b/crates/lib/tests/parametric_compilation.rs
@@ -3,8 +3,7 @@
 
 use std::f64::consts::PI;
 
-use qcs::Executable;
-use qcs_api_client_openapi::common::ClientConfiguration;
+use qcs::{client::Qcs, Executable};
 
 const BASIC_SUBSTITUTION: &str = r#"
 DECLARE ro BIT
@@ -19,8 +18,7 @@ MEASURE 0 ro[0]
 
 #[tokio::test]
 async fn basic_substitution() {
-    let mut exe =
-        Executable::from_quil(BASIC_SUBSTITUTION).with_config(ClientConfiguration::default());
+    let mut exe = Executable::from_quil(BASIC_SUBSTITUTION).with_client(Qcs::default());
     let mut parametric_measurements = Vec::with_capacity(200);
 
     let step = 2.0 * PI / 200.0;
diff --git a/crates/python/tests/qpu/test_client.py b/crates/python/tests/test_client.py
similarity index 96%
rename from crates/python/tests/qpu/test_client.py
rename to crates/python/tests/test_client.py
index 95a0b1f82..ad05c5a2c 100644
--- a/crates/python/tests/qpu/test_client.py
+++ b/crates/python/tests/test_client.py
@@ -2,7 +2,7 @@
 from urllib.parse import urlparse
 
 from qcs_sdk import QCSClient
-from qcs_sdk.qpu.client import LoadClientError, QCSClientAuthServer, QCSClientTokens
+from qcs_sdk.client import LoadClientError, QCSClientAuthServer, QCSClientTokens
 
 
 @pytest.fixture