Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/pasqal-io/cloud-sdk into alg…
Browse files Browse the repository at this point in the history
…/document-support-deprecation-timeline
  • Loading branch information
Augustinio committed Oct 7, 2024
2 parents 9b51715 + 5bdc766 commit 46c4c13
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 45 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [0.12.3]
_released `2024-10-02`_

### ✨ Added
- Allow unauthenticated users to access public device specifications

## [0.12.2] - 2024-09-11

### Changed
Expand Down
14 changes: 10 additions & 4 deletions pasqal_cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ def __init__(
email/password combination or a TokenProvider instance.
You may omit the password, you will then be prompted to enter one.
The SDK can be initialized with several authentication options:
- Option 1: No arguments -> Allows unauthenticated access to public
features.
- Option 2: `username` and `password` -> Authenticated access using a
username and password.
- Option 3: `username` only -> Prompts for password during initialization.
- Option 4 (for developers): Provide a custom `token_provider` for
token-based authentication.
Args:
username: Email of the user to login as.
password: Password of the user to login as.
Expand All @@ -85,10 +95,6 @@ def __init__(
auth0: Auth0Config object to define the auth0 tenant to target.
project_id: ID of the owner project of the batch.
"""

if not project_id:
raise ValueError("You need to provide a project_id")

self._client = Client(
project_id=project_id,
username=username,
Expand Down
2 changes: 1 addition & 1 deletion pasqal_cloud/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
# limitations under the License.


__version__ = "0.12.2"
__version__ = "0.12.3"
54 changes: 39 additions & 15 deletions pasqal_cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,33 +44,41 @@ class EmptyFilter:


class Client:
authenticator: AuthBase
authenticator: AuthBase | None

def __init__(
self,
project_id: str,
project_id: str | None = None,
username: Optional[str] = None,
password: Optional[str] = None,
token_provider: Optional[TokenProvider] = None,
endpoints: Optional[Endpoints] = None,
auth0: Optional[Auth0Conf] = None,
):
if not username and not token_provider:
raise ValueError(
"At least a username or TokenProvider object should be provided."
)
self.endpoints = self._make_endpoints(endpoints)
self._project_id = project_id
self.user_agent = f"PasqalCloudSDK/{sdk_version}"

if token_provider is not None:
self._check_token_provider(token_provider)

self.endpoints = self._make_endpoints(endpoints)

if username:
auth0 = self._make_auth0(auth0)
token_provider = self._credential_login(username, password, auth0)

self.authenticator = HTTPBearerAuthenticator(token_provider)
self.project_id = project_id
self.user_agent = f"PasqalCloudSDK/{sdk_version}"
self.authenticator = None
if token_provider:
self.authenticator = HTTPBearerAuthenticator(token_provider)

@property
def project_id(self) -> str:
if not self._project_id:
raise ValueError("You need to set a project_id.")
return self._project_id

@project_id.setter
def project_id(self, project_id: str) -> None:
self._project_id = project_id

@staticmethod
def _make_endpoints(endpoints: Optional[Endpoints]) -> Endpoints:
Expand Down Expand Up @@ -120,6 +128,12 @@ def _authenticated_request(
payload: Optional[Union[Mapping, Sequence[Mapping]]] = None,
params: Optional[Mapping[str, Any]] = None,
) -> JSendPayload:
if self.authenticator is None:
raise ValueError(
"Authentication required. Please provide your credentials when"
" initializing the client."
)

resp = requests.request(
method,
url,
Expand Down Expand Up @@ -325,7 +339,17 @@ def cancel_workload(self, workload_id: str) -> Dict[str, Any]:
return response

def get_device_specs_dict(self) -> Dict[str, str]:
response: Dict[str, str] = self._authenticated_request(
"GET", f"{self.endpoints.core}/api/v1/devices/specs"
)["data"]
return response
if self.authenticator is not None:
response: Dict[str, str] = self._authenticated_request(
"GET", f"{self.endpoints.core}/api/v1/devices/specs"
)["data"]
return response
return self.get_public_device_specs()

def get_public_device_specs(self) -> Dict[str, str]:
response = requests.request(
"GET", f"{self.endpoints.core}/api/v1/devices/public-specs"
)
response.raise_for_status()
devices = response.json()["data"]
return {device["device_type"]: device["specs"] for device in devices}
12 changes: 12 additions & 0 deletions tests/fixtures/api/v1/devices/public-specs/_.GET.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"code": 200,
"data":
[
{
"device_type": "FRESNEL",
"specs": "{\"version\":\"1\",\"channels\":[],\"name\":\"device\"}"
}
],
"message": "OK.",
"status": "success"
}
47 changes: 22 additions & 25 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@
import requests_mock
from auth0.v3.exceptions import Auth0Error

from pasqal_cloud import (
AUTH0_CONFIG,
Auth0Conf,
Endpoints,
PASQAL_ENDPOINTS,
SDK,
)
from pasqal_cloud import AUTH0_CONFIG, Auth0Conf, Endpoints, PASQAL_ENDPOINTS, SDK
from pasqal_cloud._version import __version__ as sdk_version
from pasqal_cloud.authentication import TokenProvider
from tests.test_doubles.authentication import (
Expand Down Expand Up @@ -83,6 +77,14 @@ def test_correct_new_auth0(self):
auth0=new_auth0,
)

def test_module_no_project_id(self):
sdk = SDK(username=self.username, password=self.password)
with pytest.raises(
ValueError,
match="You need to set a project_id",
):
sdk.create_batch("", [])


@patch("pasqal_cloud.client.Auth0TokenProvider", FakeAuth0AuthenticationFailure)
class TestAuthFailure(TestSDKCommonAttributes):
Expand All @@ -104,27 +106,20 @@ def test_module_bad_password(self):
)


@patch("pasqal_cloud.client.Auth0TokenProvider", FakeAuth0AuthenticationFailure)
class TestAuthInvalidClient(TestSDKCommonAttributes):
def test_module_no_project_id(self):
with pytest.raises(
ValueError,
match="You need to provide a project_id",
):
SDK(
username=self.username,
password=self.password,
)

def test_module_no_user_with_password(self):
sdk = SDK(
project_id=self.project_id,
username=self.no_username,
password=self.password,
)
with pytest.raises(
ValueError,
match="At least a username or TokenProvider object should be provided",
match="Authentication required. Please provide your credentials when "
"initializing the client.",
):
SDK(
project_id=self.project_id,
username=self.no_username,
password=self.password,
)
sdk.get_batch("fake-id")

@patch("pasqal_cloud.client.getpass")
def test_module_no_password(self, getpass):
Expand Down Expand Up @@ -163,11 +158,13 @@ def test_bad_auth0(self):
)

def test_authentication_no_credentials_provided(self):
sdk = SDK(project_id=self.project_id)
with pytest.raises(
ValueError,
match="At least a username or TokenProvider object should be provided",
match="Authentication required. Please provide your credentials when "
"initializing the client.",
):
SDK(project_id=self.project_id)
sdk.get_batch("fake-id")

@pytest.mark.filterwarnings(
"ignore:The parameters 'endpoints' and 'auth0' are deprecated, from now use"
Expand Down
27 changes: 27 additions & 0 deletions tests/test_device_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from uuid import uuid4

import pytest
import requests_mock

from pasqal_cloud import SDK
from pasqal_cloud.errors import DeviceSpecsFetchingError
Expand Down Expand Up @@ -44,3 +45,29 @@ def test_get_device_specs_error(
mock_request_exception.last_request.url
== f"{self.sdk._client.endpoints.core}/api/v1/devices/specs"
)

def test_get_public_device_specs_success(self, mock_request: requests_mock.Mocker):
"""
Test that the SDK client can be initiated without specifying credentials.
Verify that executing `get_device_specs_dict` with an unauthenticated SDK client
will request the public endpoint, while an authenticated user will request the
private endpoint.
"""

sdk_without_auth = SDK()
public_device_specs_dict = sdk_without_auth.get_device_specs_dict()

assert (
mock_request.last_request.url
== f"{sdk_without_auth._client.endpoints.core}/api/v1/devices/public-specs"
)

internal_device_specs_dict = self.sdk.get_device_specs_dict()
assert (
mock_request.last_request.url
== f"{self.sdk._client.endpoints.core}/api/v1/devices/specs"
)

assert (
public_device_specs_dict["FRESNEL"] != internal_device_specs_dict["FRESNEL"]
)

0 comments on commit 46c4c13

Please sign in to comment.