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

Issue:
Add `list_quantum_processors` helper
From https://github.com/rigetti/pyquil/issues/1495


Discussion / hints:

Fix patch:
diff --git a/crates/lib/src/api.rs b/crates/lib/src/api.rs
index 85186bf14..ece4cbad1 100644
--- a/crates/lib/src/api.rs
+++ b/crates/lib/src/api.rs
@@ -1,7 +1,7 @@
 //! This module provides convenience functions to handle compilation,
 //! translation, parameter arithmetic rewriting, and results collection.
 
-use std::{collections::HashMap, str::FromStr};
+use std::{collections::HashMap, str::FromStr, time::Duration};
 
 use num::Complex;
 use qcs_api_client_grpc::{
@@ -10,9 +10,11 @@ use qcs_api_client_grpc::{
         get_controller_job_results_request::Target, GetControllerJobResultsRequest,
     },
 };
+use qcs_api_client_openapi::apis::{quantum_processors_api, Error as OpenAPIError};
 use quil_rs::expression::Expression;
 use quil_rs::{program::ProgramError, Program};
 use serde::Serialize;
+use tokio::time::error::Elapsed;
 
 use crate::qpu::{
     self,
@@ -313,3 +315,53 @@ pub async fn retrieve_results(
         .map(ExecutionResults::from)
         .ok_or_else(|| GrpcClientError::ResponseEmpty("Controller Job Execution Results".into()))
 }
+
+/// API Errors encountered when trying to list available quantum processors.
+#[derive(Debug, thiserror::Error)]
+pub enum ListQuantumProcessorsError {
+    /// Failed the http call
+    #[error("Failed to list processors via API: {0}")]
+    ApiError(#[from] OpenAPIError<quantum_processors_api::ListQuantumProcessorsError>),
+
+    /// Pagination did not finish before timeout
+    #[error("API pagination did not finish before timeout: {0:?}")]
+    TimeoutError(#[from] Elapsed),
+}
+
+/// Query the QCS API for the names of all available quantum processors.
+/// If `None`, the default `timeout` used is 10 seconds.
+pub async fn list_quantum_processors(
+    client: &Qcs,
+    timeout: Option<Duration>,
+) -> Result<Vec<String>, ListQuantumProcessorsError> {
+    let timeout = timeout.unwrap_or_else(|| Duration::from_secs(10));
+
+    tokio::time::timeout(timeout, async move {
+        let mut quantum_processors = vec![];
+        let mut page_token = None;
+
+        loop {
+            let result = quantum_processors_api::list_quantum_processors(
+                &client.get_openapi_client(),
+                Some(100),
+                page_token.as_deref(),
+            )
+            .await?;
+
+            let mut data = result
+                .quantum_processors
+                .into_iter()
+                .map(|qpu| qpu.id)
+                .collect::<Vec<_>>();
+            quantum_processors.append(&mut data);
+
+            page_token = result.next_page_token;
+            if page_token.is_none() {
+                break;
+            }
+        }
+
+        Ok(quantum_processors)
+    })
+    .await?
+}
diff --git a/crates/python/qcs_sdk/api.pyi b/crates/python/qcs_sdk/api.pyi
index 3e1eb5252..026634236 100644
--- a/crates/python/qcs_sdk/api.pyi
+++ b/crates/python/qcs_sdk/api.pyi
@@ -166,6 +166,7 @@ async def compile(
     """
     ...
 
+
 def rewrite_arithmetic(
     native_quil: str,
 ) -> RewriteArithmeticResults:
@@ -185,6 +186,7 @@ def rewrite_arithmetic(
     """
     ...
 
+
 def build_patch_values(
     recalculation_table: RecalculationTable,
     memory: Memory,
@@ -205,6 +207,7 @@ def build_patch_values(
     """
     ...
 
+
 async def translate(
     native_quil: str,
     num_shots: int,
@@ -229,6 +232,7 @@ async def translate(
     """
     ...
 
+
 async def submit(
     program: str,
     patch_values: Dict[str, List[float]],
@@ -253,6 +257,7 @@ async def submit(
     """
     ...
 
+
 async def retrieve_results(
     job_id: str,
     quantum_processor_id: str,
@@ -275,6 +280,7 @@ async def retrieve_results(
     """
     ...
 
+
 async def get_quilc_version(
     client: Optional[QcsClient] = None,
 ) -> str:
@@ -289,3 +295,32 @@ async def get_quilc_version(
         - ``CompilationError`` When there is an issue fetching the version from the quilc compiler.
     """
     ...
+
+
+async def get_quilc_version(
+    client: Optional[QcsClient] = None,
+) -> str:
+    """
+    Returns the version number of the running quilc server.
+
+    Args:
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+    
+    Raises:
+        - ``LoadError`` When there is an issue loading the QCS Client configuration.
+        - ``CompilationError`` When there is an issue fetching the version from the quilc compiler.
+    """
+    ...
+
+async def list_quantum_processors(
+    client: Optional[QcsClient] = None,
+    timeout: Optional[float] = None,
+) -> List[str]:
+    """
+    Returns all available Quantum Processor IDs.
+
+    Args:
+        client: The QcsClient to use. Loads one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
+        timeout: Maximum duration to wait for API calls to complete, in seconds.
+    """
+    ...
\ No newline at end of file
diff --git a/crates/python/src/api.rs b/crates/python/src/api.rs
index 02ef9fb2c..3d39eb89f 100644
--- a/crates/python/src/api.rs
+++ b/crates/python/src/api.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::{collections::HashMap, time::Duration};
 
 use pyo3::{
     create_exception,
@@ -10,13 +10,14 @@ use pyo3::{
 };
 use qcs::{
     api::{
-        ExecutionResult, ExecutionResults, Register, RewriteArithmeticResult, TranslationResult,
+        list_quantum_processors, ExecutionResult, ExecutionResults, Register,
+        RewriteArithmeticResult, TranslationResult,
     },
     qpu::quilc::{CompilerOpts, TargetDevice, DEFAULT_COMPILER_TIMEOUT},
 };
 use rigetti_pyo3::{
     create_init_submodule, py_wrap_data_struct, py_wrap_error, py_wrap_type, py_wrap_union_enum,
-    wrap_error, ToPython,
+    wrap_error, ToPython, ToPythonError,
 };
 
 use crate::qpu::client::PyQcsClient;
@@ -35,6 +36,7 @@ create_init_submodule! {
         CompilationError,
         RewriteArithmeticError,
         DeviceIsaError,
+        QcsListQuantumProcessorsError,
         QcsSubmitError
     ],
     funcs: [
@@ -44,7 +46,8 @@ create_init_submodule! {
         submit,
         retrieve_results,
         build_patch_values,
-        get_quilc_version
+        get_quilc_version,
+        py_list_quantum_processors
     ],
 }
 
@@ -89,6 +92,16 @@ create_exception!(qcs, DeviceIsaError, PyValueError);
 wrap_error!(SubmitError(qcs::api::SubmitError));
 py_wrap_error!(api, SubmitError, QcsSubmitError, PyRuntimeError);
 
+wrap_error!(ListQuantumProcessorsError(
+    qcs::api::ListQuantumProcessorsError
+));
+py_wrap_error!(
+    api,
+    ListQuantumProcessorsError,
+    QcsListQuantumProcessorsError,
+    PyRuntimeError
+);
+
 #[pyfunction(client = "None", kwds = "**")]
 pub fn compile<'a>(
     py: Python<'a>,
@@ -208,3 +221,21 @@ pub fn get_quilc_version(py: Python<'_>, client: Option<PyQcsClient>) -> PyResul
         Ok(version)
     })
 }
+
+#[pyfunction(client = "None", timeout = "None")]
+#[pyo3(name = "list_quantum_processors")]
+pub fn py_list_quantum_processors(
+    py: Python<'_>,
+    client: Option<PyQcsClient>,
+    timeout: Option<f64>,
+) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let client = PyQcsClient::get_or_create_client(client).await?;
+        let timeout = timeout.map(Duration::from_secs_f64);
+        let names = list_quantum_processors(&client, timeout)
+            .await
+            .map_err(ListQuantumProcessorsError::from)
+            .map_err(ListQuantumProcessorsError::to_py_err)?;
+        Ok(names)
+    })
+}
diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs
index 36de3d7be..c1dbe7abe 100644
--- a/crates/python/src/lib.rs
+++ b/crates/python/src/lib.rs
@@ -30,7 +30,8 @@ create_init_submodule! {
         api::submit,
         api::retrieve_results,
         api::build_patch_values,
-        api::get_quilc_version
+        api::get_quilc_version,
+        api::py_list_quantum_processors
     ],
     submodules: [
         "api": api::init_submodule,


Test patch:
diff --git a/crates/python/tests/test_api.py b/crates/python/tests/test_api.py
index 7abefc608..4864d3382 100644
--- a/crates/python/tests/test_api.py
+++ b/crates/python/tests/test_api.py
@@ -48,3 +48,9 @@ def test_build_patch_values():
 async def test_get_quilc_version():
     version = await qcs_sdk.get_quilc_version()
     assert re.match(r"^([0-9]+)\.([0-9]+)\.([0-9]+)$", version)
+
+
+@pytest.mark.asyncio
+async def test_list_quantum_processors():
+    qpus = await qcs_sdk.list_quantum_processors()
+    assert isinstance(qpus, list)