Skip to content

feat!: Make Python bindings public #463

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
471bae3
wip: various publicity modifications and refactors
MarquessV Apr 18, 2024
3d3a4d7
Merge branch 'main' into 451-make-python-bindings-public
MarquessV Apr 18, 2024
f83ec80
Merge branch 'main' into 451-make-python-bindings-public
MarquessV Jul 12, 2024
939318f
derive debug on PyTranslationOptions
MarquessV Jul 12, 2024
42f12b2
add utility method for converting TranslationOptions into protobuf bytes
MarquessV Jul 17, 2024
478d68d
add methods for inspecting a PyConnectionStrategy
MarquessV Jul 17, 2024
2b4061b
update qcs dependencies
MarquessV Jul 18, 2024
cf0f95e
update rigetti-pyo3
MarquessV Jul 18, 2024
0a64f8d
Merge main, remove unused async
MarquessV Aug 27, 2024
39ef7b6
update Cargo.lock
MarquessV Aug 27, 2024
2ef36aa
replace py_sync module w/ rigetti-pyo3
MarquessV Aug 27, 2024
497d851
Apply suggestions from code review
MarquessV Aug 27, 2024
1003b40
update qcs-api-client-grpc
MarquessV Aug 27, 2024
f90eaa6
remove stale TODO
MarquessV Aug 27, 2024
7e0a5b7
reduce visibility of gRPC message size constant
MarquessV Aug 27, 2024
b23ec8b
update qcs-api-cilent packages, use uv instead of poetry
MarquessV Aug 30, 2024
33ebd8f
activate virtual environment
MarquessV Aug 30, 2024
b42ebcb
commit the change
MarquessV Aug 30, 2024
3b55aaf
activate venv
MarquessV Aug 30, 2024
a04c1a3
separate steps?
MarquessV Aug 30, 2024
caaabc4
write to GITHUB_ENV
MarquessV Aug 30, 2024
4c33b1b
try cat
MarquessV Aug 30, 2024
d1cdc85
try action
MarquessV Aug 30, 2024
db19606
remove duplicated command
MarquessV Aug 30, 2024
846f7aa
add missing dev dep
MarquessV Sep 14, 2024
53c03e0
Update qcs-api-client-rust packages
MarquessV Sep 16, 2024
b4cf81a
Merge branch 'main' into 451-make-python-bindings-public
MarquessV Sep 16, 2024
5c075da
update quil-rs
MarquessV Sep 16, 2024
80069b1
update type stubs
MarquessV Sep 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ 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.8.14"
qcs-api-client-grpc = "0.8.7"
qcs-api-client-openapi = "0.9.7"
serde_json = "1.0.86"
thiserror = "1.0.57"
tokio = "1.36.0"
Expand All @@ -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.
Expand Down
59 changes: 46 additions & 13 deletions crates/lib/src/qpu/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,7 +40,8 @@ use crate::executable::Parameters;

use crate::client::{GrpcClientError, GrpcConnection, Qcs};

const MAX_DECODING_MESSAGE_SIZE_BYTES: usize = 250 * 1024 * 1024;
/// The maximum size of a gRPC response, in bytes.
pub const MAX_DECODING_MESSAGE_SIZE_BYTES: usize = 250 * 1024 * 1024;

pub(crate) fn params_into_job_execution_configuration(
params: &Parameters,
Expand Down Expand Up @@ -435,11 +437,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 for provided 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<Duration>;

/// 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<execute_controller_job_request::Target> {
match self.connection_strategy() {
Expand All @@ -452,8 +464,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<get_controller_job_results_request::Target> {
match self.connection_strategy() {
Expand All @@ -466,8 +479,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<cancel_controller_jobs_request::Target> {
match self.connection_strategy() {
Expand All @@ -480,9 +494,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<ControllerClient<GrpcConnection>, QpuApiError> {
Expand All @@ -494,8 +508,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<GrpcConnection, QpuApiError> {
Expand Down Expand Up @@ -525,6 +539,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,
Expand All @@ -540,6 +555,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,
Expand All @@ -548,6 +564,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,
Expand All @@ -557,6 +574,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<Duration> {
self.timeout()
}
}

#[cached(
result = true,
time = 60,
Expand Down Expand Up @@ -690,7 +720,10 @@ pub enum QpuApiError {
/// The message associated with the failed job.
message: String,
},

/// Error that can occur when the gRPC status code could not be decoded.
#[error("The status code could not be decoded: {0}")]
StatusCodeDecode(String), // TODO: This error is in prost. Should we really use that as a dep
// 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}")]
Expand Down
3 changes: 2 additions & 1 deletion crates/python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,6 +40,7 @@ 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
Expand Down
11 changes: 11 additions & 0 deletions crates/python/qcs_sdk/qpu/api.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
4 changes: 4 additions & 0 deletions crates/python/qcs_sdk/qpu/translation.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
12 changes: 11 additions & 1 deletion crates/python/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ py_wrap_type! {
}

impl PyQcsClient {
pub(crate) fn get_or_create_client(client: Option<Self>) -> Qcs {
pub fn get_or_create_client(client: Option<Self>) -> Qcs {
match client {
Some(client) => client.into(),
None => Qcs::load(),
Expand Down Expand Up @@ -201,6 +201,16 @@ impl PyQcsClient {
self.as_ref().get_config().qvm_url().to_string()
}

#[getter]
pub fn auth_server(&self) -> AuthServer {
self.as_ref().get_config().auth_server().clone()
}

#[getter]
pub fn tokens(&self, py: Python<'_>) -> PyResult<Tokens> {
self.as_ref().get_config().get_tokens(py)
}

fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyObject {
match op {
CompareOp::Eq => (self == other).into_py(py),
Expand Down
23 changes: 22 additions & 1 deletion crates/python/src/qpu/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ impl PyApiExecutionOptionsBuilder {
}

py_wrap_type! {
#[derive(Default)]
#[derive(Debug, Default)]
PyConnectionStrategy(ConnectionStrategy) as "ConnectionStrategy"
}
impl_repr!(PyConnectionStrategy);
Expand All @@ -562,16 +562,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<Self> {
Ok(Self(ConnectionStrategy::EndpointId(endpoint_id)))
}

fn is_endpoint_id(&self) -> bool {
matches!(self.as_inner(), ConnectionStrategy::EndpointId(_))
}

fn get_endpoint_id(&self) -> PyResult<String> {
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),
Expand Down
Loading
Loading