diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 351ff8b1c..4086fecdf 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -20,21 +20,18 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} version: '3.20.1' - - name: Install libquil dependencies run: sudo apt install -y libffi\* libblas\* liblapack\* - name: Install libquil run: | curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | bash -s 0.3.0 - - uses: Swatinem/rust-cache@v2 - name: Install cargo-make uses: actions-rs/cargo@v1 with: command: install args: --debug cargo-make - - name: Install poetry - uses: snok/install-poetry@v1 + - uses: syphar/restore-virtualenv@v1 # may protect against linking failure: no space left on device - name: Delete huge unnecessary tools folder run: rm -rf /opt/hostedtoolcache diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 07c1a5c35..5b0e3e00f 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -19,14 +19,13 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.11' - - name: Install poetry - uses: snok/install-poetry@v1 - uses: Swatinem/rust-cache@v2 - name: Install cargo-make uses: actions-rs/cargo@v1 with: command: install args: --debug cargo-make + - uses: syphar/restore-virtualenv@v1 - name: Build qcs-sdk-python documentation uses: actions-rs/cargo@v1 with: diff --git a/Cargo.lock b/Cargo.lock index dea6ac8cd..e59f47f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1592,18 +1592,18 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lexical" -version = "6.1.1" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6" +checksum = "8ecd3381ac77c22d4e2607284ac71e44b21c21bd3785ee807d21976d54ee16f9" dependencies = [ "lexical-core", ] [[package]] name = "lexical-core" -version = "0.8.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +checksum = "0885f6cdfe75c96e45bbf1c4e49511f128201391ce3b56e60e29f5a1fadbc1c1" dependencies = [ "lexical-parse-float", "lexical-parse-integer", @@ -1614,9 +1614,9 @@ dependencies = [ [[package]] name = "lexical-parse-float" -version = "0.8.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +checksum = "924f7ec090cd4f60bd873f160b0fb69a0c80bb3a98f2e778a1893ae0e5c4b0b9" dependencies = [ "lexical-parse-integer", "lexical-util", @@ -1625,9 +1625,9 @@ dependencies = [ [[package]] name = "lexical-parse-integer" -version = "0.8.6" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +checksum = "8feab1da84a2ab0ddbbad2fb1830b755f71a9a8d996c7a1f2a553faf72aa3686" dependencies = [ "lexical-util", "static_assertions", @@ -1635,18 +1635,18 @@ dependencies = [ [[package]] name = "lexical-util" -version = "0.8.5" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +checksum = "591ce1a12ecd3b26d4121ab360a6a4483a67f05a5372add6acbfd0b65c9285d9" dependencies = [ "static_assertions", ] [[package]] name = "lexical-write-float" -version = "0.8.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +checksum = "05b0f3f9ddada5942b54e97654d535df37c9340ad66c24b50360a90619779f41" dependencies = [ "lexical-util", "lexical-write-integer", @@ -1655,9 +1655,9 @@ dependencies = [ [[package]] name = "lexical-write-integer" -version = "0.8.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +checksum = "48c6d47254ddb292771dce7697ae2be9619f8e369d01a9ccda15ef2ff50443fc" dependencies = [ "lexical-util", "static_assertions", @@ -2551,7 +2551,7 @@ dependencies = [ "num-complex", "parking_lot", "portable-atomic", - "pyo3-build-config", + "pyo3-build-config 0.20.3", "pyo3-ffi", "pyo3-macros", "unindent", @@ -2580,6 +2580,16 @@ dependencies = [ "target-lexicon", ] +[[package]] +name = "pyo3-build-config" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7879eb018ac754bba32cb0eec7526391c02c14a093121857ed09fbf1d1057d41" +dependencies = [ + "once_cell", + "target-lexicon", +] + [[package]] name = "pyo3-ffi" version = "0.20.3" @@ -2587,7 +2597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" dependencies = [ "libc", - "pyo3-build-config", + "pyo3-build-config 0.20.3", ] [[package]] @@ -2621,7 +2631,7 @@ checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" dependencies = [ "heck 0.4.1", "proc-macro2", - "pyo3-build-config", + "pyo3-build-config 0.20.3", "quote", "syn 2.0.66", ] @@ -2736,19 +2746,25 @@ dependencies = [ [[package]] name = "qcs-api-client-common" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "040552f233992abe94376304cf17dab2d79ff62d4a9ee986d75efe3d19337f57" +checksum = "a0ee577b5e5855fbd227b32e95683e863e510930710b0daefbe6214c878787c0" dependencies = [ "async-trait", "backoff", + "base64 0.22.1", "derive_builder 0.20.0", "figment", "futures", "home", "http", "jsonwebtoken", + "paste", + "pyo3", + "pyo3-asyncio", + "pyo3-build-config 0.22.1", "reqwest", + "rigetti-pyo3 0.3.6", "serde", "shellexpand", "thiserror", @@ -2762,9 +2778,9 @@ dependencies = [ [[package]] name = "qcs-api-client-grpc" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb755cd59b1b3ce99abe52ff6898ad74ca26463e94300c103cafaac4f345c4f3" +checksum = "eae7be57e9f3e82f14e97ae287715e1d0d34a33dbdc135cb2f7800a4e1de5f04" dependencies = [ "backoff", "http", @@ -2795,9 +2811,9 @@ dependencies = [ [[package]] name = "qcs-api-client-openapi" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008d06c4051dd4b3e7d5b6c1f31457c68384f8f36b05b304de6059bbd81c6128" +checksum = "f3c4df0083961178536f3e277ce4aa133bd56814c351c9fcd4dd237b35ac37ec" dependencies = [ "anyhow", "qcs-api-client-common", @@ -2823,9 +2839,10 @@ dependencies = [ "opentelemetry 0.23.0", "opentelemetry_sdk 0.23.0", "paste", + "prost", "pyo3", "pyo3-asyncio", - "pyo3-build-config", + "pyo3-build-config 0.20.3", "pyo3-log", "pyo3-opentelemetry", "pyo3-tracing-subscriber", @@ -2835,7 +2852,7 @@ dependencies = [ "qcs-api-client-grpc", "qcs-api-client-openapi", "quil-rs", - "rigetti-pyo3", + "rigetti-pyo3 0.4.1", "serde_json", "thiserror", "tokio", @@ -2850,8 +2867,8 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quil-rs" -version = "0.27.1" -source = "git+https://github.com/rigetti/quil-rs?tag=quil-py/v0.11.1#ab5b976573b38f3ba2605f57482ccadb1f13bf30" +version = "0.28.1" +source = "git+https://github.com/rigetti/quil-rs?tag=quil-py/v0.12.1#a0f2776893d3ce33eb9f29449f3b4b0e9ed24174" dependencies = [ "approx", "indexmap 2.2.6", @@ -3099,6 +3116,19 @@ dependencies = [ "time", ] +[[package]] +name = "rigetti-pyo3" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f924032d36104a859f936762576a9e6fc0811b37a1f4a8144c0b9b25ee89607b" +dependencies = [ + "num-complex", + "num-traits", + "paste", + "pyo3", + "time", +] + [[package]] name = "ring" version = "0.16.20" diff --git a/Cargo.toml b/Cargo.toml index 5eabd18f5..49c05bb18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,19 +4,19 @@ resolver = "2" [workspace.dependencies] qcs-api = "0.2.1" -qcs-api-client-common = "0.8.4" -qcs-api-client-grpc = "0.8.4" -qcs-api-client-openapi = "0.9.4" +qcs-api-client-common = "0.10.0" +qcs-api-client-grpc = "0.10.0" +qcs-api-client-openapi = "0.11.0" serde_json = "1.0.86" thiserror = "1.0.57" tokio = "1.36.0" # We specify quil-rs as a git and versioned dependency so that we can keep the version of # quil-rs used in both the Rust and Python packages in sync. The tag used should always -# be a `quil-py` tag and should be comaptible with the version specified in +# be a `quil-py` tag and should be compatible with the version specified in # `crates/python/pyproject.toml`. # The version must also be specified in order to publish to crates.io. Cargo enforces # that the specified version is the same as the version in the git repository. -quil-rs = { version = "0.27.1", git = "https://github.com/rigetti/quil-rs", tag = "quil-py/v0.11.1" } +quil-rs = { version = "0.28.1", git = "https://github.com/rigetti/quil-rs", tag = "quil-py/v0.12.1" } # ndarray is used by the `qcs` crate, but it is also used in the `python` crate via a # re-export through the numpy crate. They should be updated as a pair to keep both @@ -32,7 +32,7 @@ pyo3-opentelemetry = { version = "=0.3.2-dev.1" } pyo3-tracing-subscriber = { version = "=0.1.2-dev.1", default-features = true } pyo3-build-config = "0.20.0" -rigetti-pyo3 = { version = "0.3.1", default-features = false, features = ["time", "complex"] } +rigetti-pyo3 = { version = "0.4.1", default-features = false, features = ["complex", "time"] } # The primary intent of these options is to reduce the binary size for Python wheels # since PyPi has limits on how much storage a project can use. diff --git a/crates/lib/src/compiler/quilc.rs b/crates/lib/src/compiler/quilc.rs index 32d14b0a8..de6a20fcd 100644 --- a/crates/lib/src/compiler/quilc.rs +++ b/crates/lib/src/compiler/quilc.rs @@ -357,7 +357,7 @@ mod tests { serde_json::from_reader(File::open("tests/qvm_isa.json").unwrap()).unwrap() } - async fn rpcq_client() -> rpcq::Client { + fn rpcq_client() -> rpcq::Client { let qcs = Qcs::load(); let endpoint = qcs.get_config().quilc_url(); rpcq::Client::new(endpoint).unwrap() @@ -366,7 +366,6 @@ mod tests { #[tokio::test] async fn compare_native_quil_to_expected_output() { let output = rpcq_client() - .await .compile_program( "MEASURE 0", TargetDevice::try_from(qvm_isa()).expect("Couldn't build target device from ISA"), @@ -390,7 +389,6 @@ MEASURE 1 ro[1] let client = Qcs::load(); let client = qvm::http::HttpClient::from(&client); let output = rpcq_client() - .await .compile_program( BELL_STATE, TargetDevice::try_from(aspen_9_isa()) @@ -426,7 +424,6 @@ MEASURE 1 ro[1] #[tokio::test] async fn test_compile_declare_only() { let output = rpcq_client() - .await .compile_program( "DECLARE ro BIT[1]\n", TargetDevice::try_from(aspen_9_isa()) @@ -440,7 +437,7 @@ MEASURE 1 ro[1] #[tokio::test] async fn get_version_info_from_quilc() { - let rpcq_client = rpcq_client().await; + let rpcq_client = rpcq_client(); let version = rpcq_client .get_version_info() .expect("Should get version info from quilc"); @@ -450,7 +447,7 @@ MEASURE 1 ro[1] #[tokio::test] async fn test_conjugate_pauli_by_clifford() { - let rpcq_client = rpcq_client().await; + let rpcq_client = rpcq_client(); let request = ConjugateByCliffordRequest { pauli: PauliTerm { indices: vec![0], @@ -473,7 +470,7 @@ MEASURE 1 ro[1] #[tokio::test] async fn test_generate_randomized_benchmark_sequence() { - let rpcq_client = rpcq_client().await; + let rpcq_client = rpcq_client(); let request = RandomizedBenchmarkingRequest { depth: 2, qubits: 1, diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs index 225a67feb..ba9c5ff24 100644 --- a/crates/lib/src/executable.rs +++ b/crates/lib/src/executable.rs @@ -803,7 +803,7 @@ mod describe_get_config { use crate::client::Qcs; use crate::{compiler::rpcq, Executable}; - async fn quilc_client() -> rpcq::Client { + fn quilc_client() -> rpcq::Client { let qcs = Qcs::load(); let endpoint = qcs.get_config().quilc_url(); rpcq::Client::new(endpoint).unwrap() @@ -811,7 +811,7 @@ mod describe_get_config { #[tokio::test] async fn it_resizes_params_dynamically() { - let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client().await)); + let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client())); exe.with_parameter("foo", 0, 0.0); let params = exe.params.get("foo").unwrap().len(); @@ -834,7 +834,7 @@ mod describe_qpu_for_id { use crate::qpu; use crate::{client::Qcs, Executable}; - async fn quilc_client() -> rpcq::Client { + fn quilc_client() -> rpcq::Client { let qcs = Qcs::load(); let endpoint = qcs.get_config().quilc_url(); rpcq::Client::new(endpoint).unwrap() @@ -845,7 +845,7 @@ mod describe_qpu_for_id { // Default config has no auth, so it should try to refresh let mut exe = Executable::from_quil("") .with_qcs_client(Qcs::load()) - .with_quilc_client(Some(quilc_client().await)); + .with_quilc_client(Some(quilc_client())); let result = exe.qpu_for_id("blah").await; let Err(err) = result else { panic!("Expected an error!"); @@ -856,7 +856,7 @@ mod describe_qpu_for_id { #[tokio::test] async fn it_loads_cached_version() { - let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client().await)); + let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client())); let shots = NonZeroU16::new(17).expect("value is non-zero"); exe.shots = shots; exe.qpu = Some( @@ -881,7 +881,7 @@ mod describe_qpu_for_id { async fn it_creates_new_after_shot_change() { let original_shots = NonZeroU16::new(23).expect("value is non-zero"); let mut exe = Executable::from_quil("") - .with_quilc_client(Some(quilc_client().await)) + .with_quilc_client(Some(quilc_client())) .with_shots(original_shots); let qpu = exe.qpu_for_id("Aspen-9").await.unwrap(); @@ -898,7 +898,7 @@ mod describe_qpu_for_id { #[tokio::test] async fn it_creates_new_for_new_qpu_id() { - let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client().await)); + let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client())); let qpu = exe.qpu_for_id("Aspen-9").await.unwrap(); assert_eq!(qpu.quantum_processor_id, "Aspen-9"); diff --git a/crates/lib/src/qpu/api.rs b/crates/lib/src/qpu/api.rs index af6a281e6..026cb8054 100644 --- a/crates/lib/src/qpu/api.rs +++ b/crates/lib/src/qpu/api.rs @@ -3,6 +3,7 @@ use std::{convert::TryFrom, fmt, time::Duration}; +use async_trait::async_trait; use cached::proc_macro::cached; use derive_builder::Builder; use qcs_api_client_common::configuration::TokenError; @@ -39,6 +40,7 @@ use crate::executable::Parameters; use crate::client::{GrpcClientError, GrpcConnection, Qcs}; +/// The maximum size of a gRPC response, in bytes. const MAX_DECODING_MESSAGE_SIZE_BYTES: usize = 250 * 1024 * 1024; pub(crate) fn params_into_job_execution_configuration( @@ -443,11 +445,21 @@ pub enum ConnectionStrategy { EndpointId(String), } -/// Methods that help select and configure a controller service client given a set of -/// [`ExecutionOptions`] and QPU ID. -impl ExecutionOptions { +/// An ExecutionTarget provides methods to establish the appropriate connection to the execution +/// service. +/// +/// Implementors provide a [`ConnectionStrategy`] and timeout, the trait provides default +/// implementation for getting connections and execution targets. +#[async_trait] +pub trait ExecutionTarget<'a> { + /// The [`ConnectionStrategy`] to use to determine the connection target. + fn connection_strategy(&'a self) -> &'a ConnectionStrategy; + /// The timeout to use for requests to the target. + fn timeout(&self) -> Option; + + /// Get the [`execute_controller_job_request::Target`] for the given quantum processor ID. fn get_job_target( - &self, + &'a self, quantum_processor_id: Option<&str>, ) -> Option { match self.connection_strategy() { @@ -460,8 +472,9 @@ impl ExecutionOptions { } } + /// Get the [`get_controller_job_results_request::Target`] for the given quantum processor ID. fn get_results_target( - &self, + &'a self, quantum_processor_id: Option<&str>, ) -> Option { match self.connection_strategy() { @@ -474,8 +487,9 @@ impl ExecutionOptions { } } + /// Get the [`cancel_controller_jobs_request::Target`] for the given quantum processor ID. fn get_cancel_target( - &self, + &'a self, quantum_processor_id: Option<&str>, ) -> Option { match self.connection_strategy() { @@ -488,9 +502,9 @@ impl ExecutionOptions { } } - /// Get a controller client for the given QPU ID. - pub async fn get_controller_client( - &self, + /// Get a controller client for the given quantum processor ID. + async fn get_controller_client( + &'a self, client: &Qcs, quantum_processor_id: Option<&str>, ) -> Result, QpuApiError> { @@ -502,8 +516,8 @@ impl ExecutionOptions { } /// Get a GRPC connection to a QPU, without specifying the API to use. - pub async fn get_qpu_grpc_connection( - &self, + async fn get_qpu_grpc_connection( + &'a self, client: &Qcs, quantum_processor_id: Option<&str>, ) -> Result { @@ -533,6 +547,7 @@ impl ExecutionOptions { self.grpc_address_to_channel(&address, client) } + /// Get a channel from the given gRPC address. fn grpc_address_to_channel( &self, address: &str, @@ -548,6 +563,7 @@ impl ExecutionOptions { Ok(channel) } + /// Get the gateway address for the given quantum processor ID. async fn get_gateway_address( &self, quantum_processor_id: &str, @@ -556,6 +572,7 @@ impl ExecutionOptions { get_accessor_with_cache(quantum_processor_id, client).await } + /// Get the default endpoint address for the given quantum processor ID. async fn get_default_endpoint_address( &self, quantum_processor_id: &str, @@ -565,6 +582,19 @@ impl ExecutionOptions { } } +/// Methods that help select and configure a controller service client given a set of +/// [`ExecutionOptions`] and QPU ID. +#[async_trait] +impl<'a> ExecutionTarget<'a> for ExecutionOptions { + fn connection_strategy(&'a self) -> &'a ConnectionStrategy { + self.connection_strategy() + } + + fn timeout(&self) -> Option { + self.timeout() + } +} + #[cached( result = true, time = 60, @@ -698,7 +728,10 @@ pub enum QpuApiError { /// The message associated with the failed job. message: String, }, - + /// Error that can occur when the gRPC status code cannot be decoded. + #[error("The status code could not be decoded: {0}")] + StatusCodeDecode(String), + // just for the error type? /// Error that can occur if a numeric status identifier cannot be converted /// into a known status type. #[error("The request returned an invalid status: {status}. {message}")] diff --git a/crates/lib/src/qvm/execution.rs b/crates/lib/src/qvm/execution.rs index ff31a5abd..798cda969 100644 --- a/crates/lib/src/qvm/execution.rs +++ b/crates/lib/src/qvm/execution.rs @@ -81,7 +81,7 @@ mod describe_execution { use super::{Execution, Parameters}; use crate::{client::Qcs, qvm}; - async fn qvm_client() -> qvm::http::HttpClient { + fn qvm_client() -> qvm::http::HttpClient { let qcs = Qcs::load(); qvm::http::HttpClient::from(&qcs) } @@ -98,7 +98,7 @@ mod describe_execution { NonZeroU16::new(1).expect("value is non-zero"), HashMap::new(), ¶ms, - &qvm_client().await, + &qvm_client(), ) .await; if let Err(e) = result { @@ -120,7 +120,7 @@ mod describe_execution { NonZeroU16::new(1).expect("value is non-zero"), HashMap::new(), ¶ms, - &qvm_client().await, + &qvm_client(), ) .await; if let Err(e) = result { diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml index 05ed39333..4565ac896 100644 --- a/crates/python/Cargo.toml +++ b/crates/python/Cargo.toml @@ -21,7 +21,7 @@ crate-type = ["cdylib", "rlib"] async-trait = "0.1.73" qcs = { path = "../lib", features = ["tracing-opentelemetry"] } qcs-api.workspace = true -qcs-api-client-common.workspace = true +qcs-api-client-common = { workspace = true, features = ["python"] } qcs-api-client-grpc.workspace = true qcs-api-client-openapi.workspace = true pyo3.workspace = true @@ -40,7 +40,12 @@ once_cell = "1.18.0" opentelemetry = { version = "0.23.0" } opentelemetry_sdk = { version = "0.23.0" } tracing = { version = "0.1.37" } +prost = "0.12.6" [build-dependencies] pyo3-build-config.workspace = true pyo3-tracing-subscriber = { version = "=0.1.2-dev.1", features = ["layer-otel-otlp-file", "layer-otel-otlp", "stubs"], default-features = false } + +[lints.clippy] +too_many_arguments = "allow" + diff --git a/crates/python/Makefile.toml b/crates/python/Makefile.toml index 22f9cf9dd..4874a965f 100644 --- a/crates/python/Makefile.toml +++ b/crates/python/Makefile.toml @@ -1,53 +1,85 @@ [env] +PYTHONPATH = { script = [ "python -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())'" ] } RUST_BACKTRACE = 0 -[tasks.pre-test-docker-up] -command = "docker" -args = ["compose", "up", "-d"] +[tasks.check-venv] +description = "Check if a virtual environment is activated" +script = [ + ''' + if [ -z "$VIRTUAL_ENV" ]; then + echo "No virtual environment activated. Please activate one." + exit 1 + else + echo "Virtual environment is active." + fi + ''' +] -[tasks.poetry-install] -command = "poetry" -args = ["install"] +[tasks.install-uv] +dependencies = ["check-venv"] +description = "Install dependencies using uv" +command = "pip" +args = ["install", "uv"] + +[tasks.install-deps] +dependencies = ["install-uv"] +description = "Install project dependencies using uv." +script = [ + ''' + uv pip compile pyproject.toml --all-extras > requirements-dev.txt + uv pip install -r requirements-dev.txt + rm requirements-dev.txt + ''' +] -[tasks.install-lib] -command = "poetry" -args = ["run", "maturin", "develop"] +[tasks.install-python-package] +dependencies = ["check-venv", "install-deps"] +description = "Build the python package and install to the active virtual environment." +command = "maturin" +args = ["develop"] + +[tasks.pre-test-docker-up] +script = [ + ''' + docker compose up -d || { echo 'Warning: `docker compose up` failed. Assuming QVM and quilc are running already.'; true; } + ''' +] [tasks.format-tracing-subscriber] -command = "poetry" -args = ["run", "black", "qcs_sdk/_tracing_subscriber"] +command = "black" +args = ["qcs_sdk/_tracing_subscriber"] [tasks.post-test] -command = "docker" -args = ["compose", "down"] +script = [ + ''' + docker compose down || true + ''' +] [tasks.test] -command = "poetry" -args = ["run", "pytest", "tests"] +command = "pytest" +args = ["tests"] [tasks.test-session] -command = "poetry" -args = ["run", "pytest", "tests", "--with-qcs-session"] +command = "pytest" +args = ["tests", "--with-qcs-session"] [tasks.test-execution] -command = "poetry" -args = ["run", "pytest", "tests", "--with-qcs-session", "--with-qcs-execution"] +command = "pytest" +args = ["tests", "--with-qcs-session", "--with-qcs-execution"] [tasks.pytest-flow] dependencies = [ "pre-test-docker-up", - "poetry-install", - "install-lib", + "install-python-package", "format-tracing-subscriber", "test", "post-test", ] [tasks.stubtest] -command = "poetry" +command = "stubtest" args = [ - "run", - "stubtest", "--allowlist", ".stubtest-allowlist", "--allowlist", @@ -57,9 +89,8 @@ args = [ [tasks.stubtest-flow] dependencies = [ - "poetry-install", - "install-lib", - "fomat-tracing-subscriber", + "install-python-package", + "format-tracing-subscriber", "stubtest", ] @@ -73,7 +104,7 @@ alias = "dev-flow" dependencies = ["pytest-flow", "stubtest"] [tasks.docs] -dependencies = ["poetry-install", "install-lib"] -command = "poetry" -args = ["run", "pdoc", "-o", "build/docs", "qcs_sdk", "!qcs_sdk.qcs_sdk", "--logo", "https://qcs.rigetti.com/static/img/rigetti-logo.svg"] +dependencies = ["install-python-package"] +command = "pdoc" +args = ["-o", "build/docs", "qcs_sdk", "!qcs_sdk.qcs_sdk", "--logo", "https://qcs.rigetti.com/static/img/rigetti-logo.svg"] diff --git a/crates/python/pyproject.toml b/crates/python/pyproject.toml index e105cb8f6..1c4f8dc72 100644 --- a/crates/python/pyproject.toml +++ b/crates/python/pyproject.toml @@ -20,21 +20,8 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Operating System :: OS Independent", ] -dependencies = ["quil>=0.11.2"] -# 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.19.4-rc.0" -description = "Python interface for the QCS Rust SDK" -readme = "README.md" -authors = [ - "Rigetti Computing ", - "Mark Skilbeck ", - "Marquess Valdez ", - "Randall Fulton ", -] +dependencies = ["quil>=0.11.2", "qcs-api-client-common>=0.10.0"] [tool.maturin] features = ["pyo3/extension-module"] @@ -42,23 +29,25 @@ bindings = "pyo3" compatibility = "linux" sdist-include = ["README.md"] -[tool.poetry.dependencies] -python = "^3.8" -opentelemetry-api = { version = "^1.25.0", optional = true } - -[tool.poetry.group.dev.dependencies] -numpy = "^1.24.1" -maturin = "^1.2.3" -pytest = "^7.1.3" -pytest-asyncio = "^0.19.0" -black = "^22.8.0" -syrupy = "^3.0.6" -mypy = "^1.4.1" -pdoc = "^14.1.0" -opentelemetry-sdk = "^1.25.0" - -[tool.poetry.extras] -tracing-opentelemetry = ["opentelemetry-api"] +[project.optional-dependencies] +pyquil = [ + "pyquil==4.14.2", +] +dev = [ + "black >= 24.8.0", + "opentelemetry-sdk==1.25.0", + "pytest >= 8.1.1", + "pytest-asyncio >= 0.23.6", + "pytest-mock >= 3.14.0", + "pytest-sugar >= 1.0.0", + "pytest-clarity >= 1.0.1", + "syrupy >= 4.0.0", + "maturin == 1.5.1", + "numpy >= 1.24.1", + "pdoc >= 14.6.1", + "ruff >= 0.3.5", + "mypy >= 1.4.1", +] [build-system] requires = ["maturin>=1.0.0,<2.0.0"] diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py index a4123cdf1..8c674f36e 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.py @@ -15,4 +15,3 @@ __doc__ = layers.__doc__ __all__ = getattr(layers, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi index 741a427e2..6fdd52b85 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/__init__.pyi @@ -13,18 +13,18 @@ from __future__ import annotations from typing import TYPE_CHECKING -from . import file as file +from . import file as file from . import otel_otlp_file as otel_otlp_file from . import otel_otlp as otel_otlp if TYPE_CHECKING: - from typing import Union + from typing import Union - Config = Union[ - file.Config, - otel_otlp_file.Config, - otel_otlp.Config, - ] - """ + Config = Union[ + file.Config, + otel_otlp_file.Config, + otel_otlp.Config, + ] + """ One of the supported layer configurations that may be set on the subscriber configuration. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py index f1ac331e1..80cff859b 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.py @@ -15,5 +15,3 @@ __doc__ = file.__doc__ __all__ = getattr(file, "__all__", []) - - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi index 7caa5c2e0..8054827fe 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/file/__init__.pyi @@ -39,4 +39,3 @@ class Config: :param json: Whether or not to format the output as JSON. Defaults to `True`. """ ... - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py index 7b1f6baf6..8a3bcdd6e 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.py @@ -15,4 +15,3 @@ __doc__ = otel_otlp.__doc__ __all__ = getattr(otel_otlp, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi index 5dde69007..453e2d8e8 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp/__init__.pyi @@ -47,8 +47,6 @@ class Resource: schema_url: Optional[str] = None, ) -> "Resource": ... - - @final class Config: """ @@ -98,14 +96,14 @@ class Config: ... if TYPE_CHECKING: - from typing import List, Union + from typing import List, Union ResourceValueArray = Union[List[bool], List[int], List[float], List[str]] """ An array of `ResourceValue`s. This array is homogenous, so all values must be of the same type. """ - ResourceValue= Union[bool, int, float, str, ResourceValueArray] + ResourceValue = Union[bool, int, float, str, ResourceValueArray] """ A value that can be added to a `Resource`. """ diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py index 7f928b757..4d7ea6443 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.py @@ -15,4 +15,3 @@ __doc__ = otel_otlp_file.__doc__ __all__ = getattr(otel_otlp_file, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi index 613126b30..31e2f495f 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/layers/otel_otlp_file/__init__.pyi @@ -32,4 +32,3 @@ class Config: and then `RUST_LOG` environment variable. If all of these values are empty, no spans will be exported. """ ... - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py index fb711d054..5f4924199 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.py @@ -15,4 +15,3 @@ __doc__ = subscriber.__doc__ __all__ = getattr(subscriber, "__all__", []) - diff --git a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi index 548de0cc5..e0894d0a4 100644 --- a/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi +++ b/crates/python/qcs_sdk/_tracing_subscriber/subscriber/__init__.pyi @@ -22,4 +22,3 @@ class Config: """ def __new__(cls, *, layer: layers.Config) -> "Config": ... - diff --git a/crates/python/qcs_sdk/client.pyi b/crates/python/qcs_sdk/client.pyi index e6fb5c6ad..9b6b96cc0 100644 --- a/crates/python/qcs_sdk/client.pyi +++ b/crates/python/qcs_sdk/client.pyi @@ -1,4 +1,4 @@ -from typing import Optional, final +from typing import Callable, Optional, final class LoadClientError(RuntimeError): """Error encountered while loading the QCS API client configuration from the environment configuration.""" @@ -18,9 +18,8 @@ class QCSClient: def __new__( cls, - tokens: Optional[QCSClientTokens] = None, + oauth_session: Optional[OAuthSession] = None, api_url: Optional[str] = None, - auth_server: Optional[QCSClientAuthServer] = None, grpc_api_url: Optional[str] = None, quilc_url: Optional[str] = None, qvm_url: Optional[str] = None, @@ -62,55 +61,95 @@ class QCSClient: def qvm_url(self) -> str: """URL to access the QVM.""" ... + @property + def oauth_session(self) -> OAuthSession: + """Get a copy of the OAuth session.""" @final -class QCSClientAuthServer: - """Authentication server configuration for the QCS API.""" +class OAuthSession: + def __new__( + cls, + grant_payload: RefreshToken | ClientCredentials | ExternallyManaged, + auth_server: AuthServer, + access_token: str | None = None, + ) -> OAuthSession: ... + @property + def access_token(self) -> str: + """Get the current access token. - def __new__(cls, client_id: str, issuer: str) -> "QCSClientAuthServer": + This is an unvalidated copy of the access token. Meaning it can become stale, or may already be stale. See the `validate` `request_access_token` and methods. """ - Manually define authentication server parameters. - :param client_id: The OAuth application client ID. If `None`, a default value is used. - :param issuer: The OAuth token issuer url. If `None`, a default value is used. - """ - ... @property - def client_id(self) -> str: ... - @client_id.setter - def client_id(self, value: str): ... + def auth_server(self) -> AuthServer: + """The auth server.""" + @property - def issuer(self) -> str: ... - @issuer.setter - def issuer(self, value: str): ... + def payload(self) -> RefreshToken | ClientCredentials: + """Get the payload used to request an access token.""" -@final -class QCSClientTokens: - """Authentication tokens for the QCS API.""" + def request_access_token(self) -> str: + """Request a new access token.""" - def __new__( - cls, - bearer_access_token: str, - refresh_token: str, - auth_server: Optional[QCSClientAuthServer] = None, - ) -> "QCSClientTokens": - """ - Manually define authentication session tokens. + async def request_access_token_async(self) -> str: + """Request a new access token.""" + + def validate(self) -> str: + """Validate the current access token, returning it if it is valid. - :param bearer_access_token: The session token from an OAuth issuer. - :param refresh_token: A credential to refresh the bearer_access_token when it expires. - :param auth_server: The OAuth server configuration. If `None`, default values are loaded. + If the token is invalid, a `ValueError` will be raised with a description of why the token failed validation. """ - ... + +@final +class AuthServer: + def __new__(cls, client_id: str, issuer: str) -> AuthServer: ... + @staticmethod + def default() -> AuthServer: + """Get the default Okta auth server.""" + + @property + def client_id(self) -> str: + """The client's Okta ID.""" + @property - def bearer_access_token(self) -> Optional[str]: ... - @bearer_access_token.setter - def bearer_access_token(self, value: Optional[str]): ... + def issuer(self) -> str: + """The Okta issuer URL.""" + +@final +class RefreshToken: + def __new__(cls, refresh_token: str) -> RefreshToken: ... @property - def refresh_token(self) -> Optional[str]: ... + def refresh_token(self) -> str: + """The refresh token.""" @refresh_token.setter - def refresh_token(self, value: Optional[str]): ... + def refresh_token(self, refresh_token: str): + """Set the refresh token.""" + +@final +class ClientCredentials: + def __new__(cls, client_id: str, client_secret: str) -> ClientCredentials: ... + @property + def client_id(self) -> str: + """The client ID.""" @property - def auth_server(self) -> Optional[QCSClientAuthServer]: ... - @auth_server.setter - def auth_server(self, value: Optional[QCSClientAuthServer]): ... + def client_secret(self) -> str: + """The client secret.""" + +@final +class ExternallyManaged: + def __new__( + cls, refresh_function: Callable[[AuthServer], str] + ) -> ExternallyManaged: + """Manages access tokens by utilizing a user-provided refresh function. + + The refresh function should return a valid access token, or raise an exception if it cannot. + + .. testcode:: + from qcs_apiclient_common.configuration import AuthServer, ExternallyManaged, OAuthSession + + def refresh_function(auth_server: AuthServer) -> str: + return "my_access_token" + + externally_managed = ExternallyManaged(refresh_function) + session = OAuthSession(externally_managed, AuthServer.default()) + """ diff --git a/crates/python/qcs_sdk/qpu/api.pyi b/crates/python/qcs_sdk/qpu/api.pyi index 524b2054e..78483a9d5 100644 --- a/crates/python/qcs_sdk/qpu/api.pyi +++ b/crates/python/qcs_sdk/qpu/api.pyi @@ -434,10 +434,21 @@ class ConnectionStrategy: @staticmethod def gateway() -> ConnectionStrategy: """Connect through the publicly accessbile gateway.""" + def is_gateway(self) -> bool: + """True if the ConnectionStrategy is to connect to the QCS gateway.""" @staticmethod def direct_access() -> ConnectionStrategy: """Connect directly to the default endpoint, bypassing the gateway. Should only be used when you have direct network access and an active reservation.""" + def is_direct_access(self) -> bool: + """True if the ConnectionStrategy is to use direct access.""" @staticmethod def endpoint_id(endpoint_id: str) -> ConnectionStrategy: """Connect directly to a specific endpoint using its ID.""" + def is_endpoint_id(self) -> bool: + """True if the ConnectionStrategy is to connect to a particular endpoint ID.""" + def get_endpoint_id(self) -> str: + """Get the endpoint ID used by the ConnectionStrategy. + + Raises an error if this ConnectionStrategy doesn't use a specific endpoint ID. + """ diff --git a/crates/python/qcs_sdk/qpu/isa.pyi b/crates/python/qcs_sdk/qpu/isa.pyi index 751a05b9a..0c91d5472 100644 --- a/crates/python/qcs_sdk/qpu/isa.pyi +++ b/crates/python/qcs_sdk/qpu/isa.pyi @@ -14,17 +14,30 @@ class GetISAError(RuntimeError): ... @final -class Family(Enum): +class Family: """ The architecture family identifier of an ``InstructionSetArchitecture``. Value "Full" implies that each node is connected to every other (fully-connected architecture). """ - - NONE = "NONE" - Full = "Full" - Aspen = "Aspen" - Ankaa = "Ankaa" + def is_ankaa(self) -> bool: ... + def is_aspen(self) -> bool: ... + def is_full(self) -> bool: ... + def is_unknown(self) -> bool: ... + def is_none(self) -> bool: ... + def as_unknown(self) -> Optional[str]: ... + def to_unknown(self) -> str: ... + @staticmethod + def from_unknown(inner: str) -> "Family": ... + def inner(self) -> str: ... + @staticmethod + def new_ankaa() -> "Family": ... + @staticmethod + def new_aspen() -> "Family": ... + @staticmethod + def new_full() -> "Family": ... + @staticmethod + def new_none() -> "Family": ... @final class Node: diff --git a/crates/python/qcs_sdk/qpu/translation.pyi b/crates/python/qcs_sdk/qpu/translation.pyi index 73edc1113..8beeb7edc 100644 --- a/crates/python/qcs_sdk/qpu/translation.pyi +++ b/crates/python/qcs_sdk/qpu/translation.pyi @@ -155,3 +155,7 @@ class TranslationOptions: :param: allow_frame_redefinition: If True, allow defined frames to differ from Rigetti defaults. Only available to certain users. Otherwise, only ``INITIAL-FREQUENCY`` and ``CHANNEL-DELAY`` may be modified. """ + def encode_as_protobuf(self) -> bytes: + """ + Serialize these translation options into the Protocol Buffer format. + """ diff --git a/crates/python/src/client.rs b/crates/python/src/client.rs index 5c69fa4b6..c27d63c0c 100644 --- a/crates/python/src/client.rs +++ b/crates/python/src/client.rs @@ -1,13 +1,15 @@ +use pyo3::{exceptions::PyValueError, pyfunction}; use qcs_api_client_common::configuration::{ - AuthServer, ClientConfigurationBuilder, ClientConfigurationBuilderError, Tokens, + AuthServer, ClientConfigurationBuilder, ClientConfigurationBuilderError, ClientCredentials, + ExternallyManaged, OAuthSession, RefreshToken, }; use rigetti_pyo3::{ - create_init_submodule, py_wrap_data_struct, py_wrap_error, py_wrap_type, + create_init_submodule, py_function_sync_async, py_wrap_error, py_wrap_type, pyo3::{ - conversion::IntoPy, exceptions::PyRuntimeError, pyclass::CompareOp, pymethods, - types::PyString, Py, PyObject, PyResult, Python, + conversion::IntoPy, exceptions::PyRuntimeError, pyclass::CompareOp, pymethods, PyObject, + PyResult, Python, }, - wrap_error, ToPythonError, + wrap_error, PyWrapper, ToPythonError, }; use qcs::client::{self, Qcs}; @@ -15,8 +17,11 @@ use qcs::client::{self, Qcs}; create_init_submodule! { classes: [ PyQcsClient, - PyQcsClientAuthServer, - PyQcsClientTokens + OAuthSession, + AuthServer, + RefreshToken, + ClientCredentials, + ExternallyManaged ], errors: [ LoadClientError, @@ -36,78 +41,12 @@ py_wrap_error!( PyRuntimeError ); -// The fields on qcs_api_client_common::client::AuthServer are not public. -py_wrap_type!( - PyQcsClientAuthServer(AuthServer) as "QCSClientAuthServer" -); - -#[pymethods] -impl PyQcsClientAuthServer { - #[new] - #[pyo3(signature = (client_id = None, issuer = None))] - pub fn new(client_id: Option, issuer: Option) -> Self { - let mut auth_server = AuthServer::default(); - if let Some(client_id) = client_id { - auth_server.set_client_id(client_id); - } - if let Some(issuer) = issuer { - auth_server.set_issuer(issuer); - } - Self(auth_server) - } - - #[getter(client_id)] - fn get_client_id(&self) -> String { - self.0.client_id().to_string() - } - - #[setter(client_id)] - fn set_client_id(&mut self, value: String) { - self.0.set_client_id(value); - } - - #[getter(issuer)] - fn get_issuer(&self) -> String { - self.0.issuer().to_string() - } - - #[setter(issuer)] - fn set_issuer(&mut self, value: String) { - self.0.set_issuer(value); - } -} - -py_wrap_data_struct! { - PyQcsClientTokens(Tokens) as "QCSClientTokens" { - bearer_access_token: String => Py, - refresh_token: String => Py, - auth_server: AuthServer => PyQcsClientAuthServer - } -} - -#[pymethods] -impl PyQcsClientTokens { - #[new] - #[pyo3(signature = (bearer_access_token, refresh_token, auth_server = None))] - pub fn new( - bearer_access_token: String, - refresh_token: String, - auth_server: Option, - ) -> Self { - Self(Tokens { - bearer_access_token, - refresh_token, - auth_server: auth_server.map(Into::into).unwrap_or_default(), - }) - } -} - py_wrap_type! { PyQcsClient(Qcs) as "QCSClient"; } impl PyQcsClient { - pub(crate) fn get_or_create_client(client: Option) -> Qcs { + pub fn get_or_create_client(client: Option) -> Qcs { match client { Some(client) => client.into(), None => Qcs::load(), @@ -126,31 +65,26 @@ impl PyQcsClient { #[new] #[pyo3(signature = ( /, - tokens = None, + oauth_session = None, api_url = None, - auth_server = None, grpc_api_url = None, quilc_url = None, qvm_url = None ))] pub fn new( - tokens: Option, + oauth_session: Option, api_url: Option, - auth_server: Option, grpc_api_url: Option, quilc_url: Option, qvm_url: Option, ) -> PyResult { let mut builder = ClientConfigurationBuilder::default(); - if let Some(tokens) = tokens { - builder.tokens(Some(tokens.into())); + if let Some(session) = oauth_session { + builder.oauth_session(Some(session)); } if let Some(api_url) = api_url { builder.api_url(api_url); } - if let Some(auth_server) = auth_server { - builder.auth_server(auth_server.into()); - } if let Some(grpc_api_url) = grpc_api_url { builder.grpc_api_url(grpc_api_url); } @@ -201,6 +135,11 @@ impl PyQcsClient { self.as_ref().get_config().qvm_url().to_string() } + #[getter] + pub fn oauth_session(&self, py: Python<'_>) -> PyResult { + py_get_oauth_session(py, self.clone()) + } + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyObject { match op { CompareOp::Eq => (self == other).into_py(py), @@ -209,3 +148,11 @@ impl PyQcsClient { } } } + +py_function_sync_async! { + #[pyfunction] + async fn get_oauth_session(client: PyQcsClient) -> PyResult { + client.as_inner().get_config().oauth_session().await.map_err(|e| PyValueError::new_err(e.to_string())) + + } +} diff --git a/crates/python/src/compiler/quilc.rs b/crates/python/src/compiler/quilc.rs index 0592d3c29..d7fbcbf4f 100644 --- a/crates/python/src/compiler/quilc.rs +++ b/crates/python/src/compiler/quilc.rs @@ -6,8 +6,8 @@ use qcs::compiler::quilc::{ use qcs_api_client_openapi::models::InstructionSetArchitecture; use quil_rs::quil::Quil; use rigetti_pyo3::{ - create_init_submodule, impl_repr, py_wrap_data_struct, py_wrap_error, py_wrap_struct, - py_wrap_type, + create_init_submodule, impl_repr, py_function_sync_async, py_wrap_data_struct, py_wrap_error, + py_wrap_struct, py_wrap_type, pyo3::{ exceptions::{PyRuntimeError, PyValueError}, pyclass, pyfunction, pymethods, @@ -17,7 +17,6 @@ use rigetti_pyo3::{ wrap_error, PyWrapper, ToPythonError, }; -use crate::py_sync::py_function_sync_async; use crate::qpu::isa::PyInstructionSetArchitecture; create_init_submodule! { diff --git a/crates/python/src/executable.rs b/crates/python/src/executable.rs index 6e56e9da8..0eaf82e64 100644 --- a/crates/python/src/executable.rs +++ b/crates/python/src/executable.rs @@ -4,7 +4,7 @@ use opentelemetry::trace::FutureExt; use pyo3::{pyclass, FromPyObject}; use qcs::{Error, Executable, ExecutionData, JobHandle, Service}; use rigetti_pyo3::{ - impl_as_mut_for_wrapper, py_wrap_error, py_wrap_simple_enum, py_wrap_type, + impl_as_mut_for_wrapper, py_async, py_sync, py_wrap_error, py_wrap_simple_enum, py_wrap_type, pyo3::{exceptions::PyRuntimeError, pymethods, types::PyDict, Py, PyAny, PyResult, Python}, wrap_error, PyWrapper, ToPython, ToPythonError, }; @@ -14,7 +14,6 @@ use tracing::instrument; use crate::{ compiler::quilc::{PyCompilerOpts, PyQuilcClient}, execution_data::PyExecutionData, - py_sync::{py_async, py_sync}, qpu::{api::PyExecutionOptions, translation::PyTranslationOptions}, }; diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs index 18af0c172..d272dbcaf 100644 --- a/crates/python/src/lib.rs +++ b/crates/python/src/lib.rs @@ -1,7 +1,7 @@ use std::sync::Mutex; use pyo3::prelude::*; -use rigetti_pyo3::create_init_submodule; +use rigetti_pyo3::{create_init_submodule, py_sync}; use executable::ExecutionError; use execution_data::RegisterMatrixConversionError; @@ -16,7 +16,6 @@ pub mod qvm; pub mod register_data; pub(crate) mod from_py; -pub(crate) mod py_sync; create_init_submodule! { classes: [ @@ -74,5 +73,5 @@ fn reset_logging() { #[pyfunction] #[pyo3(name = "_gather_diagnostics")] fn gather_diagnostics(py: Python<'_>) -> PyResult { - py_sync::py_sync!(py, async { Ok(qcs::diagnostics::get_report().await) }) + py_sync!(py, async { Ok(qcs::diagnostics::get_report().await) }) } diff --git a/crates/python/src/py_sync.rs b/crates/python/src/py_sync.rs deleted file mode 100644 index c47fdd5f6..000000000 --- a/crates/python/src/py_sync.rs +++ /dev/null @@ -1,133 +0,0 @@ -/// Spawn and block on a future using the pyo3 tokio runtime. -/// Useful for returning a synchronous `PyResult`. -/// -/// -/// When used like the following: -/// ```rs -/// async fn say_hello(name: String) -> String { -/// format!("hello {name}") -/// } -/// -/// #[pyo3(name="say_hello")] -/// pub fn py_say_hello(name: String) -> PyResult { -/// py_sync!(say_hello(name)) -/// } -/// ``` -/// -/// Becomes the associated "synchronous" python call: -/// ```py -/// assert say_hello("Rigetti") == "hello Rigetti" -/// ``` -macro_rules! py_sync { - ($py: ident, $body: expr) => {{ - $py.allow_threads(|| { - let runtime = ::pyo3_asyncio::tokio::get_runtime(); - let handle = runtime.spawn($body); - - runtime.block_on(async { - tokio::select! { - result = handle => result.map_err(|err| ::pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))?, - signal_err = async { - // A 100ms loop delay is a bit arbitrary, but seems to - // balance CPU usage and SIGINT responsiveness well enough. - let delay = ::std::time::Duration::from_millis(100); - loop { - ::pyo3::Python::with_gil(|py| { - py.check_signals() - })?; - ::tokio::time::sleep(delay).await; - } - } => signal_err, - } - }) - }) - }}; -} - -/// Convert a rust future into a Python awaitable using -/// `pyo3_asyncio::tokio::future_into_py` -macro_rules! py_async { - ($py: ident, $body: expr) => { - ::pyo3_asyncio::tokio::future_into_py($py, $body) - }; -} - -/// Given a single implementation of an async function, -/// create that function as private and two pyfunctions -/// named after it that can be used to invoke either -/// blocking or async variants of the same function. -/// -/// In order to export the function to Python using pyo3 -/// you must include the `#[pyfunction]` attribute. This -/// isn't included in the macro by default since one may -/// wish to annotate `#[pyfunction]` with additional -/// arguments. -/// -/// The given function will be spawned on a Rust event loop -/// this means functions like [`pyo3::Python::with_gil`] -/// should not be used, as acquiring Python's global -/// interpreter lock from a Rust runtime -/// isn't possible. -/// -/// This macro cannot be used when lifetime specifiers are -/// required, or the pyfunction bodies need additional -/// parameter handling besides simply calling out to -/// the underlying `py_async` or `py_sync` macros. -/// -/// ```rs -/// // ... becomes python package "things" -/// create_init_submodule! { -/// funcs: [ -/// py_do_thing, -/// py_do_thing_async, -/// ] -/// } -/// -/// py_function_sync_async! { -/// #[pyfunction] -/// #[args(timeout = "None")] -/// async fn do_thing(timeout: Option) -> PyResult { -/// // ... sleep for timeout ... -/// Ok(String::from("done")) -/// } -/// } -/// ``` -/// -/// becomes in python: -/// ```py -/// from things import do_thing, do_thing_async -/// assert do_thing() == "done" -/// assert await do_thing_async() == "done" -/// ``` -macro_rules! py_function_sync_async { - ( - $(#[$meta: meta])+ - async fn $name: ident($($(#[$arg_meta: meta])*$arg: ident : $kind: ty),* $(,)?) $(-> $ret: ty)? $body: block - ) => { - async fn $name($($arg: $kind,)*) $(-> $ret)? { - $body - } - - ::paste::paste! { - $(#[$meta])+ - #[allow(clippy::too_many_arguments)] - #[pyo3(name = $name "")] - pub fn [< py_ $name >](py: ::pyo3::Python<'_> $(, $(#[$arg_meta])*$arg: $kind)*) $(-> $ret)? { - use opentelemetry::trace::FutureExt; - $crate::py_sync::py_sync!(py, $name($($arg),*).with_current_context()) - } - - $(#[$meta])+ - #[pyo3(name = $name "_async")] - #[allow(clippy::too_many_arguments)] - pub fn [< py_ $name _async >](py: ::pyo3::Python<'_> $(, $(#[$arg_meta])*$arg: $kind)*) -> ::pyo3::PyResult<&::pyo3::PyAny> { - use opentelemetry::trace::FutureExt; - $crate::py_sync::py_async!(py, $name($($arg),*).with_current_context()) - } - } - }; -} - -pub(crate) use py_async; -pub(crate) use py_function_sync_async; -pub(crate) use py_sync; diff --git a/crates/python/src/qpu/api.rs b/crates/python/src/qpu/api.rs index ac38e9561..ee47f8bba 100644 --- a/crates/python/src/qpu/api.rs +++ b/crates/python/src/qpu/api.rs @@ -19,12 +19,10 @@ use qcs_api_client_grpc::models::controller::{ data_value, readout_values, ControllerJobExecutionResult, }; use rigetti_pyo3::{ - create_init_submodule, impl_as_mut_for_wrapper, impl_repr, num_complex, py_wrap_error, - py_wrap_type, py_wrap_union_enum, wrap_error, PyWrapper, ToPythonError, + create_init_submodule, impl_as_mut_for_wrapper, impl_repr, num_complex, py_function_sync_async, + py_wrap_error, py_wrap_type, py_wrap_union_enum, wrap_error, PyWrapper, ToPythonError, }; -use crate::py_sync::py_function_sync_async; - use crate::client::PyQcsClient; use super::result_data::PyMemoryValues; @@ -576,7 +574,7 @@ impl PyApiExecutionOptionsBuilder { } py_wrap_type! { - #[derive(Default)] + #[derive(Debug, Default)] #[pyo3(module = "qcs_sdk.qpu.api")] PyConnectionStrategy(ConnectionStrategy) as "ConnectionStrategy" } @@ -595,16 +593,37 @@ impl PyConnectionStrategy { Self(ConnectionStrategy::Gateway) } + fn is_gateway(&self) -> bool { + matches!(self.as_inner(), ConnectionStrategy::Gateway) + } + #[staticmethod] fn direct_access() -> Self { Self(ConnectionStrategy::DirectAccess) } + fn is_direct_access(&self) -> bool { + matches!(self.as_inner(), ConnectionStrategy::DirectAccess) + } + #[staticmethod] fn endpoint_id(endpoint_id: String) -> PyResult { Ok(Self(ConnectionStrategy::EndpointId(endpoint_id))) } + fn is_endpoint_id(&self) -> bool { + matches!(self.as_inner(), ConnectionStrategy::EndpointId(_)) + } + + fn get_endpoint_id(&self) -> PyResult { + match self.as_inner() { + ConnectionStrategy::EndpointId(id) => Ok(id.clone()), + _ => Err(PyValueError::new_err( + "ConnectionStrategy is not an EndpointId", + )), + } + } + fn __richcmp__(&self, py: Python<'_>, other: &Self, op: CompareOp) -> PyObject { match op { CompareOp::Eq => (self.as_inner() == other.as_inner()).into_py(py), diff --git a/crates/python/src/qpu/isa.rs b/crates/python/src/qpu/isa.rs index 367672662..c1ca99d85 100644 --- a/crates/python/src/qpu/isa.rs +++ b/crates/python/src/qpu/isa.rs @@ -3,7 +3,8 @@ use qcs_api_client_openapi::models::{ OperationSite, Parameter, }; use rigetti_pyo3::{ - create_init_submodule, py_wrap_data_struct, py_wrap_error, py_wrap_simple_enum, + create_init_submodule, py_function_sync_async, py_wrap_data_struct, py_wrap_error, + py_wrap_union_enum, pyo3::{ exceptions::{PyRuntimeError, PyValueError}, prelude::*, @@ -15,7 +16,7 @@ use rigetti_pyo3::{ use qcs::qpu::get_isa; -use crate::{client::PyQcsClient, py_sync::py_function_sync_async}; +use crate::client::PyQcsClient; create_init_submodule! { classes: [ @@ -45,12 +46,13 @@ py_wrap_error!(isa, RustSerializeIsaError, SerializeISAError, PyValueError); wrap_error!(RustGetIsaError(qcs::qpu::GetIsaError)); py_wrap_error!(isa, RustGetIsaError, GetISAError, PyRuntimeError); -py_wrap_simple_enum! { +py_wrap_union_enum! { PyFamily(Family) as "Family" { - None as NONE, - Full as Full, - Aspen as Aspen, - Ankaa as Ankaa + none: None, + full: Full, + aspen: Aspen, + ankaa: Ankaa, + unknown: Unknown => String } } diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs index 3c3307bd4..bdfdf7773 100644 --- a/crates/python/src/qpu/mod.rs +++ b/crates/python/src/qpu/mod.rs @@ -1,7 +1,9 @@ use std::time::Duration; use pyo3::{exceptions::PyRuntimeError, pyfunction, PyResult}; -use rigetti_pyo3::{create_init_submodule, py_wrap_error, wrap_error, ToPythonError}; +use rigetti_pyo3::{ + create_init_submodule, py_function_sync_async, py_wrap_error, wrap_error, ToPythonError, +}; pub use result_data::{PyQpuResultData, PyReadoutValues, RawQpuReadoutData}; @@ -11,7 +13,6 @@ mod result_data; pub mod translation; use crate::client::PyQcsClient; -use crate::py_sync::py_function_sync_async; use self::result_data::PyMemoryValues; diff --git a/crates/python/src/qpu/translation.rs b/crates/python/src/qpu/translation.rs index 341035c35..2cd61f37b 100644 --- a/crates/python/src/qpu/translation.rs +++ b/crates/python/src/qpu/translation.rs @@ -1,12 +1,19 @@ //! Translating programs. use std::{collections::HashMap, time::Duration}; +use prost::Message; +use pyo3::types::PyBytes; +use pyo3::Python; use pyo3::{exceptions::PyRuntimeError, pyclass, pyfunction, pymethods, PyResult}; use qcs::qpu::translation::TranslationOptions; -use qcs_api_client_grpc::services::translation::translation_options::TranslationBackend as ApiTranslationBackend; -use rigetti_pyo3::{create_init_submodule, py_wrap_error, py_wrap_simple_enum, ToPythonError}; - -use crate::py_sync::py_function_sync_async; +use qcs_api_client_grpc::services::translation::{ + translation_options::TranslationBackend as ApiTranslationBackend, + TranslationOptions as ApiTranslationOptions, +}; +use rigetti_pyo3::{ + create_init_submodule, py_function_sync_async, py_wrap_error, py_wrap_simple_enum, + ToPythonError, +}; use crate::client::PyQcsClient; @@ -73,7 +80,7 @@ py_wrap_simple_enum! { } } -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] #[pyclass(name = "TranslationOptions")] pub struct PyTranslationOptions(TranslationOptions); @@ -152,6 +159,11 @@ impl PyTranslationOptions { Self(builder) } + fn encode_as_protobuf<'a>(&'a self, py: Python<'a>) -> &'a PyBytes { + let options: ApiTranslationOptions = self.0.clone().into(); + PyBytes::new(py, options.encode_to_vec().as_slice()) + } + fn __repr__(&self) -> String { format!("{:?}", self.0) } diff --git a/crates/python/src/qvm/api.rs b/crates/python/src/qvm/api.rs index d5d3ddcd2..f90900c62 100644 --- a/crates/python/src/qvm/api.rs +++ b/crates/python/src/qvm/api.rs @@ -1,7 +1,8 @@ use std::{collections::HashMap, num::NonZeroU16}; +use crate::register_data::PyRegisterData; + use super::{PyQvmOptions, RustQvmError}; -use crate::{py_sync::py_function_sync_async, register_data::PyRegisterData}; use pyo3::{ pymethods, @@ -19,7 +20,7 @@ use qcs::{ RegisterData, }; use rigetti_pyo3::{ - create_init_submodule, impl_repr, py_wrap_data_struct, py_wrap_type, + create_init_submodule, impl_repr, py_function_sync_async, py_wrap_data_struct, py_wrap_type, pyo3::{pyfunction, PyResult}, PyTryFrom, PyWrapper, PyWrapperMut, ToPythonError, }; diff --git a/crates/python/src/qvm/mod.rs b/crates/python/src/qvm/mod.rs index 2dfb560a2..5231b084b 100644 --- a/crates/python/src/qvm/mod.rs +++ b/crates/python/src/qvm/mod.rs @@ -4,14 +4,15 @@ use qcs::{ RegisterData, }; use rigetti_pyo3::{ - create_init_submodule, impl_as_mut_for_wrapper, impl_repr, py_wrap_error, py_wrap_type, + create_init_submodule, impl_as_mut_for_wrapper, impl_repr, py_function_sync_async, + py_wrap_error, py_wrap_type, pyo3::{exceptions::PyRuntimeError, prelude::*, Python}, wrap_error, PyTryFrom, PyWrapper, PyWrapperMut, ToPython, ToPythonError, }; use std::num::NonZeroU16; use std::{collections::HashMap, time::Duration}; -use crate::{py_sync::py_function_sync_async, register_data::PyRegisterData}; +use crate::register_data::PyRegisterData; mod api; diff --git a/crates/python/tests/compiler/__snapshots__/test_quilc.ambr b/crates/python/tests/compiler/__snapshots__/test_quilc.ambr index 7140896e7..3f3a6f11a 100644 --- a/crates/python/tests/compiler/__snapshots__/test_quilc.ambr +++ b/crates/python/tests/compiler/__snapshots__/test_quilc.ambr @@ -1,3 +1,4 @@ +# serializer version: 1 # name: test_compile_program ''' DECLARE ro BIT[2] diff --git a/crates/python/tests/test_client.py b/crates/python/tests/test_client.py index 09b5e4ecd..2c42c3b81 100644 --- a/crates/python/tests/test_client.py +++ b/crates/python/tests/test_client.py @@ -4,8 +4,9 @@ from qcs_sdk.client import ( QCSClient, LoadClientError, - QCSClientAuthServer, - QCSClientTokens, + OAuthSession, + RefreshToken, + AuthServer, ) @@ -51,18 +52,13 @@ def test_client_broken_raises(): QCSClient.load(profile_name="broken") -def test_client_auth_server_can_be_manually_defined(): +def test_client_oauth_session_can_be_manually_defined(): """Ensures that pyo3 usage is correct.""" - auth_server = QCSClientAuthServer(client_id="foo", issuer="bar") - assert auth_server.client_id == "foo" - assert auth_server.issuer == "bar" - - -def test_client_tokens_can_be_manually_defined(): - """Ensures that pyo3 usage is correct.""" - auth_server = QCSClientTokens(bearer_access_token="foo", refresh_token="bar") - assert auth_server.bearer_access_token == "foo" - assert auth_server.refresh_token == "bar" + auth_server = AuthServer("url", "issuer") + session = OAuthSession(RefreshToken("refresh"), auth_server, "access") + assert session.payload.refresh_token == "refresh" + assert session.access_token == "access" + assert session.auth_server == auth_server def test_client_constructor():