Skip to main content

qcs_api_client_common/configuration/
py.rs

1#![allow(unused_qualifications)]
2#![allow(non_local_definitions, reason = "necessary for pyo3::pymethods")]
3
4use pyo3::{
5    exceptions::PyValueError,
6    prelude::*,
7    types::{PyFunction, PyString},
8};
9use rigetti_pyo3::{create_init_submodule, impl_repr, py_function_sync_async, sync::Awaitable};
10use tokio_util::sync::CancellationToken;
11
12#[cfg(feature = "stubs")]
13use pyo3_stub_gen::derive::{gen_stub_pyfunction, gen_stub_pymethods};
14
15use crate::configuration::{
16    secrets::{DEFAULT_SECRETS_PATH, SECRETS_PATH_VAR},
17    settings::{DEFAULT_SETTINGS_PATH, SETTINGS_PATH_VAR},
18    ClientConfigurationBuilderError, API_URL_VAR, DEFAULT_API_URL, DEFAULT_GRPC_API_URL,
19    DEFAULT_PROFILE_NAME, DEFAULT_QUILC_URL, DEFAULT_QVM_URL, GRPC_API_URL_VAR, PROFILE_NAME_VAR,
20    QUILC_URL_VAR, QVM_URL_VAR,
21};
22use crate::errors;
23
24use super::{
25    error::TokenError,
26    secrets::{SecretAccessToken, SecretRefreshToken},
27    settings::AuthServer,
28    tokens::{ClientCredentials, ClientSecret, ExternallyManaged, PkceFlow},
29    ClientConfiguration, ClientConfigurationBuilder, LoadError, OAuthGrant, OAuthSession,
30    RefreshToken, TokenDispatcher,
31};
32
33create_init_submodule! {
34    classes: [
35        ClientConfiguration,
36        ClientConfigurationBuilder,
37        AuthServer,
38        OAuthSession,
39        RefreshToken,
40        ClientCredentials,
41        ClientSecret,
42        ExternallyManaged,
43        PkceFlow,
44        SecretAccessToken,
45        SecretRefreshToken,
46        TokenDispatcher
47    ],
48
49    consts: [
50        API_URL_VAR,
51        DEFAULT_API_URL,
52        DEFAULT_GRPC_API_URL,
53        DEFAULT_PROFILE_NAME,
54        DEFAULT_QUILC_URL,
55        DEFAULT_QVM_URL,
56        DEFAULT_SECRETS_PATH,
57        DEFAULT_SETTINGS_PATH,
58        GRPC_API_URL_VAR,
59        PROFILE_NAME_VAR,
60        QUILC_URL_VAR,
61        QVM_URL_VAR,
62        SECRETS_PATH_VAR,
63        SETTINGS_PATH_VAR
64    ],
65
66    errors: [
67        errors::ClientConfigurationBuilderError,
68        errors::ConfigurationError,
69        errors::LoadError,
70        errors::TokenError
71    ],
72
73    funcs: [
74        py_get_oauth_session,
75        py_get_oauth_session_async,
76        py_get_bearer_access_token,
77        py_get_bearer_access_token_async,
78        py_request_access_token,
79        py_request_access_token_async
80    ],
81
82}
83
84#[cfg(feature = "stubs")]
85#[derive(IntoPyObject)]
86struct Final<T>(T);
87
88#[cfg(feature = "stubs")]
89impl<T> pyo3_stub_gen::PyStubType for Final<T> {
90    fn type_output() -> pyo3_stub_gen::TypeInfo {
91        pyo3_stub_gen::TypeInfo::with_module("typing.Final", "typing".into())
92    }
93}
94
95/// Adds module-level `str` to the `qcs_api_client_common.configuration` stub file.
96macro_rules! stub_consts {
97    ( $($name:ident),* ) => {
98        $(
99            #[cfg(feature = "stubs")]
100            ::pyo3_stub_gen::module_variable!(
101                "qcs_api_client_common.configuration",
102                stringify!($name),
103                Final<&str>,
104                Final($name)
105            );
106        )*
107    };
108}
109
110stub_consts!(
111    API_URL_VAR,
112    DEFAULT_API_URL,
113    DEFAULT_GRPC_API_URL,
114    DEFAULT_PROFILE_NAME,
115    DEFAULT_QUILC_URL,
116    DEFAULT_QVM_URL,
117    DEFAULT_SECRETS_PATH,
118    DEFAULT_SETTINGS_PATH,
119    GRPC_API_URL_VAR,
120    PROFILE_NAME_VAR,
121    QUILC_URL_VAR,
122    QVM_URL_VAR,
123    SECRETS_PATH_VAR,
124    SETTINGS_PATH_VAR
125);
126
127/// Manual implementation to extract tokens from Python objects.
128///
129/// For Python functions that require a `SecretRefreshToken`,
130/// users can provide a Python `str`, a `RefreshToken`, or a `SecretRefreshToken`.
131impl FromPyObject<'_, '_> for SecretRefreshToken {
132    type Error = PyErr;
133
134    fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
135        if let Ok(token) = obj.cast::<PyString>() {
136            Ok(Self::__new__(token.extract()?))
137        } else if let Ok(token) = obj.cast::<RefreshToken>() {
138            Ok(token.borrow().refresh_token.clone())
139        } else if let Ok(token) = obj.cast::<Self>() {
140            Ok(token.borrow().clone())
141        } else {
142            Err(PyValueError::new_err(
143                "expected str | SecretRefreshToken | RefreshToken",
144            ))
145        }
146    }
147}
148
149impl FromPyObject<'_, '_> for SecretAccessToken {
150    type Error = PyErr;
151
152    fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
153        if let Ok(token) = obj.cast::<PyString>() {
154            Ok(Self::__new__(token.extract()?))
155        } else if let Ok(token) = obj.cast::<Self>() {
156            Ok(token.borrow().clone())
157        } else {
158            Err(PyValueError::new_err("expected str | SecretAccessToken"))
159        }
160    }
161}
162
163impl FromPyObject<'_, '_> for ClientSecret {
164    type Error = PyErr;
165
166    fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
167        if let Ok(token) = obj.cast::<PyString>() {
168            Ok(Self::__new__(token.extract()?))
169        } else if let Ok(token) = obj.cast::<Self>() {
170            Ok(token.borrow().clone())
171        } else {
172            Err(PyValueError::new_err("expected str | ClientSecret"))
173        }
174    }
175}
176
177impl_repr!(RefreshToken);
178
179#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
180#[pymethods]
181impl RefreshToken {
182    #[new]
183    const fn __new__(refresh_token: SecretRefreshToken) -> Self {
184        Self::new(refresh_token)
185    }
186}
187
188impl_repr!(ClientCredentials);
189
190#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
191#[pymethods]
192impl ClientCredentials {
193    #[new]
194    fn __new__(client_id: String, client_secret: String) -> Self {
195        Self::new(client_id, ClientSecret::from(client_secret))
196    }
197}
198
199impl_repr!(ExternallyManaged);
200
201#[cfg_attr(not(feature = "stubs"), optipy::strip_pyo3(only_stubs))]
202#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
203#[pymethods]
204impl ExternallyManaged {
205    #[new]
206    fn __new__(
207        #[gen_stub(
208            override_type(
209                type_repr="collections.abc.Callable[[AuthServer], str]",
210                imports=("collections.abc")
211            )
212        )]
213        refresh_function: Py<PyFunction>,
214    ) -> Self {
215        #[allow(trivial_casts)] // Compilation fails without the cast.
216        // The provided refresh function will panic if there is an issue with the refresh function.
217        // This raises a `PanicException` within Python.
218        let refresh_closure = move |auth_server: AuthServer| {
219            let refresh_function = Python::attach(|py| refresh_function.clone_ref(py));
220            Box::pin(async move {
221                Python::attach(|py| {
222                    let result = refresh_function.call1(py, (auth_server,));
223                    match result {
224                        Ok(value) => value
225                            .extract::<String>(py)
226                            .map_or_else(|_| panic!("ExternallyManaged refresh function returned an unexpected type. Expected a string, got {value:?}"), Ok),
227                        Err(err) => Err(Box::<dyn std::error::Error + Send + Sync>::from(err))
228                    }
229                })
230            }) as super::tokens::RefreshResult
231        };
232
233        Self::new(refresh_closure)
234    }
235}
236
237impl_repr!(PkceFlow);
238
239#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
240#[pymethods]
241impl PkceFlow {
242    #[new]
243    fn __new__(py: Python<'_>, auth_server: AuthServer) -> PyResult<Self> {
244        pyo3_async_runtimes::tokio::run(py, async move {
245            let cancel_token = cancel_token_with_ctrl_c();
246            Self::new_login_flow(cancel_token, &auth_server)
247                .await
248                .map_err(|err| LoadError::from(err).into())
249        })
250    }
251}
252
253#[cfg(feature = "stubs")]
254pyo3_stub_gen::impl_stub_type!(
255    OAuthGrant = RefreshToken | ClientConfiguration | ExternallyManaged | PkceFlow
256);
257
258impl_repr!(OAuthSession);
259
260#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
261#[pymethods]
262impl OAuthSession {
263    #[new]
264    #[pyo3(signature = (payload, auth_server, access_token = None))]
265    const fn __new__(
266        payload: OAuthGrant,
267        auth_server: AuthServer,
268        access_token: Option<SecretAccessToken>,
269    ) -> Self {
270        Self::new(payload, auth_server, access_token)
271    }
272
273    #[pyo3(name = "validate")]
274    fn py_validate(&self) -> Result<SecretAccessToken, TokenError> {
275        self.validate()
276    }
277
278    #[pyo3(name = "request_access_token")]
279    fn py_request_access_token(&self, py: Python<'_>) -> PyResult<SecretAccessToken> {
280        py_request_access_token(py, self.clone())
281    }
282
283    #[pyo3(name = "request_access_token_async")]
284    fn py_request_access_token_async<'py>(
285        &self,
286        py: Python<'py>,
287    ) -> PyResult<Awaitable<'py, SecretAccessToken>> {
288        py_request_access_token_async(py, self.clone())
289    }
290}
291
292py_function_sync_async! {
293    #[cfg_attr(feature = "stubs", gen_stub_pyfunction(module = "qcs_api_client_common.configuration"))]
294    #[pyfunction]
295    async fn get_oauth_session(tokens: Option<TokenDispatcher>) -> PyResult<OAuthSession> {
296        Ok(tokens.ok_or(TokenError::NoRefreshToken)?.tokens().await)
297    }
298}
299
300py_function_sync_async! {
301    #[cfg_attr(feature = "stubs", gen_stub_pyfunction(module = "qcs_api_client_common.configuration"))]
302    #[pyfunction]
303    async fn get_bearer_access_token(configuration: ClientConfiguration) -> PyResult<SecretAccessToken> {
304        configuration.get_bearer_access_token().await.map_err(PyErr::from)
305    }
306}
307
308py_function_sync_async! {
309    #[cfg_attr(feature = "stubs", gen_stub_pyfunction(module = "qcs_api_client_common.configuration"))]
310    #[pyfunction]
311    async fn request_access_token(session: OAuthSession) -> PyResult<SecretAccessToken> {
312        session.clone().request_access_token().await.cloned().map_err(PyErr::from)
313    }
314}
315
316impl_repr!(ClientConfiguration);
317
318#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
319#[pymethods]
320impl ClientConfiguration {
321    #[new]
322    #[pyo3(signature = (
323            api_url = None, grpc_api_url = None, quilc_url = None, qvm_url = None,
324            oauth_session = None,
325            ))]
326    fn __new__(
327        api_url: Option<String>,
328        grpc_api_url: Option<String>,
329        quilc_url: Option<String>,
330        qvm_url: Option<String>,
331        oauth_session: Option<OAuthSession>,
332    ) -> Self {
333        let mut builder = ClientConfigurationBuilder::default();
334
335        if let Some(api_url) = api_url {
336            builder.api_url(api_url);
337        }
338
339        if let Some(grpc_api_url) = grpc_api_url {
340            builder.grpc_api_url(grpc_api_url);
341        }
342
343        if let Some(quilc_url) = quilc_url {
344            builder.quilc_url(quilc_url);
345        }
346
347        if let Some(qvm_url) = qvm_url {
348            builder.qvm_url(qvm_url);
349        }
350
351        builder.oauth_session(oauth_session);
352
353        builder
354            .build()
355            .expect("our builder is valid regardless of which URLs are set")
356    }
357
358    #[staticmethod]
359    #[pyo3(name = "load_default")]
360    fn py_load_default(_py: Python<'_>) -> Result<Self, LoadError> {
361        Self::load_default()
362    }
363
364    #[staticmethod]
365    #[pyo3(name = "load_default_with_login")]
366    fn py_load_default_with_login(py: Python<'_>) -> PyResult<Self> {
367        pyo3_async_runtimes::tokio::run(py, async move {
368            let cancel_token = cancel_token_with_ctrl_c();
369            Self::load_with_login(cancel_token, None)
370                .await
371                .map_err(Into::into)
372        })
373    }
374
375    #[staticmethod]
376    #[pyo3(name = "builder")]
377    fn py_builder() -> ClientConfigurationBuilder {
378        ClientConfigurationBuilder::default()
379    }
380
381    #[staticmethod]
382    #[pyo3(name = "load_profile")]
383    fn py_load_profile(_py: Python<'_>, profile_name: String) -> Result<Self, LoadError> {
384        Self::load_profile(profile_name)
385    }
386
387    #[pyo3(name = "get_bearer_access_token")]
388    fn py_get_bearer_access_token(&self, py: Python<'_>) -> PyResult<SecretAccessToken> {
389        py_get_bearer_access_token(py, self.clone())
390    }
391
392    #[pyo3(name = "get_bearer_access_token_async")]
393    fn py_get_bearer_access_token_async<'py>(
394        &self,
395        py: Python<'py>,
396    ) -> PyResult<Awaitable<'py, SecretAccessToken>> {
397        py_get_bearer_access_token_async(py, self.clone())
398    }
399
400    /// Get the configured tokens.
401    ///
402    /// # Errors
403    ///
404    /// - Raises a `TokenError` if there is a problem fetching the tokens
405    pub fn get_oauth_session(&self, py: Python<'_>) -> PyResult<OAuthSession> {
406        py_get_oauth_session(py, self.oauth_session.clone())
407    }
408
409    #[allow(clippy::needless_pass_by_value)] // self_ must be passed by value
410    fn get_oauth_session_async<'py>(
411        self_: PyRefMut<'py, Self>,
412        py: Python<'py>,
413    ) -> PyResult<Awaitable<'py, OAuthSession>> {
414        py_get_oauth_session_async(py, self_.oauth_session.clone())
415    }
416}
417
418#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
419#[pymethods]
420impl ClientConfigurationBuilder {
421    #[new]
422    fn __new__() -> Self {
423        Self::default()
424    }
425
426    /// The [`OAuthSession`] to use to authenticate with the QCS API.
427    ///
428    /// When set to [`None`], the configuration will not manage an OAuth Session, and access to the
429    /// QCS API will be limited to unauthenticated routes.
430    #[setter]
431    fn set_oauth_session(&mut self, oauth_session: Option<OAuthSession>) {
432        self.oauth_session = Some(oauth_session.map(Into::into));
433    }
434
435    #[pyo3(name = "build")]
436    fn py_build(&self) -> Result<ClientConfiguration, ClientConfigurationBuilderError> {
437        self.build()
438    }
439}
440
441impl_repr!(AuthServer);
442
443#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
444#[pymethods]
445impl AuthServer {
446    #[new]
447    #[pyo3(signature = (client_id, issuer, scopes = None))]
448    const fn __new__(client_id: String, issuer: String, scopes: Option<Vec<String>>) -> Self {
449        Self::new(client_id, issuer, scopes)
450    }
451
452    #[staticmethod]
453    #[pyo3(name = "default")]
454    fn py_default() -> Self {
455        Self::default()
456    }
457}
458
459fn cancel_token_with_ctrl_c() -> CancellationToken {
460    let cancel_token = CancellationToken::new();
461    let cancel_token_ctrl_c = cancel_token.clone();
462    tokio::spawn(cancel_token.clone().run_until_cancelled_owned(async move {
463        match tokio::signal::ctrl_c().await {
464            Ok(()) => cancel_token_ctrl_c.cancel(),
465            Err(error) => eprintln!("Failed to register signal handler: {error}"),
466        }
467    }));
468    cancel_token
469}