Skip to content
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

[DEP] Bump Pydantic to V2.6 #116

Merged
merged 17 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 19 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

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

## [0.7.1] - 2024-04-11

### Added

- Upgrading Pydantic requirement from V1.10.x to >= V2.6.0.
- Updated documentation.

## [0.7.0] - 2024-03-05

### Added
Expand All @@ -14,26 +21,26 @@ All notable changes to this project will be documented in this file.

### Added

- Added feature to "rebatch" for a closed batch
- Added `parent_id` param to batch and job
- Updated documentation
- Added feature to "rebatch" for a closed batch.
- Added `parent_id` param to batch and job.
- Updated documentation.

## [0.5.0] - 2024-02-05

### Added

- Added feature to create an "open" batch.
- To create an open batch, set the `complete` argument to `True` in the `create_batch` method of the SDK
- To add jobs to an open batch, use the `add_jobs` method
- To create an open batch, set the `complete` argument to `True` in the `create_batch` method of the SDK.
- To add jobs to an open batch, use the `add_jobs` method.
- Updated documentation to add examples to create open batches.
- The `wait` argument now waits for all the jobs to be terminated instead of waiting for the batch to be terminated.

## [0.4.3] - 2024-12-08

### Added

- CRUCIAL BUGFIX - download results properly from result link
- Added exception case for results download
- CRUCIAL BUGFIX - download results properly from result link.
- Added exception case for results download.

## [0.4.2] - 2023-10-09

Expand All @@ -50,24 +57,24 @@ All notable changes to this project will be documented in this file.

### Added

- Added `result_link` field to `Workload` object
- Added `result_link` field to `Workload` object.

### Changed

- `get_workload` now targets v2 of workloads endpoints
- `result` is built from `result_link` where results are downloaded from temp s3 link
- `get_workload` now targets v2 of workloads endpoints.
- `result` is built from `result_link` where results are downloaded from temp s3 link.

## [0.4.0] - 2023-10-02

### Added

- Added exception classes for all possible failures (mostly related to client errors).
- Added try-catch to corresponding methods to raise proper error
- Added try-catch to corresponding methods to raise proper error.

### Changed

- Use `raise_for_status` on response from client before returning `data` to get accurate exception.
- Bumped minor as new exceptions are raised
- Bumped minor as new exceptions are raised.

### Removed

Expand Down
5 changes: 3 additions & 2 deletions pasqal_cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import datetime
import time
from requests.exceptions import HTTPError
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from warnings import warn

from requests.exceptions import HTTPError

from pasqal_cloud.authentication import TokenProvider
from pasqal_cloud.batch import Batch, RESULT_POLLING_INTERVAL
from pasqal_cloud.client import Client, EmptyFilter
Expand Down
2 changes: 1 addition & 1 deletion pasqal_cloud/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

__version__ = "0.7.0"
__version__ = "0.7.1"
3 changes: 1 addition & 2 deletions pasqal_cloud/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ class TokenProviderError(Exception):


class TokenProvider(ABC):
def __init__(self, *args: list[Any], **kwargs: dict):
...
def __init__(self, *args: list[Any], **kwargs: dict): ...

@abstractmethod
def get_token(self) -> str:
Expand Down
69 changes: 43 additions & 26 deletions pasqal_cloud/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@
from typing import Any, Dict, List, Optional, Type, Union
from warnings import warn

from pydantic import BaseModel, Extra, root_validator, validator
from pydantic import (
BaseModel,
ConfigDict,
field_validator,
model_validator,
PrivateAttr,
ValidationInfo,
)
from requests import HTTPError

from pasqal_cloud.client import Client
from pasqal_cloud.device import EmulatorType
from pasqal_cloud.device.configuration import BaseConfig, EmuFreeConfig, EmuTNConfig
from pasqal_cloud.errors import (
BatchCancellingError,
BatchFetchingError,
BatchSetCompleteError,
BatchCancellingError,
JobCreationError,
JobFetchingError,
JobRetryError,
)
from pasqal_cloud.job import CreateJob, Job
Expand All @@ -25,7 +31,7 @@ class Batch(BaseModel):
"""Class to load batch data return by the API.

A batch groups up several jobs with the same sequence. When a batch is assigned to
a QPU, all its jobs are ran sequentially and no other batch can be assigned to the
a QPU, all its jobs are run sequentially and no other batch can be assigned to the
device until all its jobs are done and declared complete.

Attributes:
Expand Down Expand Up @@ -63,36 +69,41 @@ class Batch(BaseModel):
user_id: str
priority: int
status: str
webhook: Optional[str]
_client: Client
_client: Client = PrivateAttr(default=None)
sequence_builder: str
start_datetime: Optional[str]
end_datetime: Optional[str]
device_status: Optional[str]
ordered_jobs: List[Job] = []
jobs_count: int = 0
jobs_count_per_status: Dict[str, int] = {}
webhook: Optional[str] = None
Mildophin marked this conversation as resolved.
Show resolved Hide resolved
start_datetime: Optional[str] = None
end_datetime: Optional[str] = None
device_status: Optional[str] = None
parent_id: Optional[str] = None
configuration: Union[BaseConfig, Dict[str, Any], None] = None

class Config:
extra = Extra.allow
arbitrary_types_allowed = True
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)

@root_validator(pre=True)
def _build_ordered_jobs(cls, values: Dict[str, Any]) -> Dict[str, Any]:
"""This root validator will modify the 'jobs' attribute which is a list
def __init__(self, **data: Any) -> None:
"""
Workaround to make the private attribute '_client' working
like we need with Pydantic V2, more information on:
https://docs.pydantic.dev/latest/concepts/models/#private-model-attributes
"""
super().__init__(**data)
self._client = data["_client"]

@model_validator(mode="before")
def _build_ordered_jobs(cls, data: Dict[str, Any]) -> Dict[str, Any]:
"""This root validator will modify the 'jobs' attribute, which is a list
of jobs dictionaries ordered by creation time before instantiation.
It will duplicate the value of 'jobs' in a new attribute 'ordered_jobs'
to keep the jobs ordered by creation time.
"""
ordered_jobs_list = []
jobs_received = values.get("jobs", [])
for job in jobs_received:
job_dict = {**job, "_client": values["_client"]}
ordered_jobs_list.append(job_dict)
values["ordered_jobs"] = ordered_jobs_list
return values
jobs_received = data.get("jobs", [])
data["ordered_jobs"] = [
{**job, "_client": data["_client"]} for job in jobs_received
]
return data

# Ticket (#704), to be removed or updated
@property
Expand All @@ -107,18 +118,24 @@ def jobs(self) -> Dict[str, Job]:
)
return {job.id: job for job in self.ordered_jobs}

@validator("configuration", pre=True)
@jobs.setter
def jobs(self, _: Any) -> None:
# Override the jobs setter to be a no-op.
# `jobs` is a read-only attribute which is derived from the `ordered_jobs` key.
pass

@field_validator("configuration", mode="before")
def _load_configuration(
cls,
configuration: Union[Dict[str, Any], BaseConfig, None],
values: Dict[str, Any],
info: ValidationInfo,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the variable name changes?

Copy link
Collaborator Author

@Mildophin Mildophin Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name changed for the same reason as I explained on your first comment, to match Pydantic documentation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok in the documentation to keep things generic but I think for our purpose here it's more readable and maintainable to keep configuration and not v.

) -> Optional[BaseConfig]:
if not isinstance(configuration, dict):
return configuration
conf_class: Type[BaseConfig] = BaseConfig
if values["device_type"] == EmulatorType.EMU_TN.value:
if info.data["device_type"] == EmulatorType.EMU_TN.value:
conf_class = EmuTNConfig
elif values["device_type"] == EmulatorType.EMU_FREE.value:
elif info.data["device_type"] == EmulatorType.EMU_FREE.value:
conf_class = EmuFreeConfig
return conf_class.from_dict(configuration)

Expand Down
17 changes: 12 additions & 5 deletions pasqal_cloud/job.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Dict, List, Optional, TypedDict, Union

from pydantic import BaseModel, Extra
from pydantic import BaseModel, ConfigDict, PrivateAttr
from requests import HTTPError

from pasqal_cloud.client import Client
Expand Down Expand Up @@ -37,7 +37,7 @@ class Job(BaseModel):
id: str
project_id: str
status: str
_client: Client
_client: Client = PrivateAttr(default=None)
created_at: str
updated_at: str
errors: Optional[List[str]] = None
Expand All @@ -50,9 +50,16 @@ class Job(BaseModel):
group_id: Optional[str] = None
parent_id: Optional[str] = None

class Config:
extra = Extra.allow
arbitrary_types_allowed = True
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)

def __init__(self, **data: Any) -> None:
"""
Workaround to make the private attribute '_client' working
like we need with Pydantic V2, more information on:
https://docs.pydantic.dev/latest/concepts/models/#private-model-attributes
"""
super().__init__(**data)
self._client = data["_client"]

def cancel(self) -> Dict[str, Any]:
"""Cancel the current job on the PCS."""
Expand Down
26 changes: 18 additions & 8 deletions pasqal_cloud/workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import Any, Dict, List, Optional

import requests
from pydantic import BaseModel, Extra, validator
from pydantic import BaseModel, ConfigDict, field_validator, PrivateAttr
from pydantic_core.core_schema import ValidationInfo
from requests import HTTPError

from pasqal_cloud.client import Client
Expand Down Expand Up @@ -41,7 +42,7 @@ class Workload(BaseModel):
id: str
project_id: str
status: str
_client: Client
_client: Client = PrivateAttr(default=None)
backend: str
workload_type: str
config: Dict[str, Any]
Expand All @@ -53,9 +54,18 @@ class Workload(BaseModel):
result_link: Optional[str] = None
result: Optional[Dict[str, Any]] = None

class Config:
extra = Extra.allow
arbitrary_types_allowed = True
model_config = ConfigDict(
extra="allow", arbitrary_types_allowed=True, validate_default=True
)

def __init__(self, **data: Any) -> None:
"""
Workaround to make the private attribute '_client' working
like we need with Pydantic V2, more information on:
https://docs.pydantic.dev/latest/concepts/models/#private-model-attributes
"""
super().__init__(**data)
self._client = data["_client"]

def cancel(self) -> Dict[str, Any]:
"""Cancel the current job on the PCS."""
Expand All @@ -66,11 +76,11 @@ def cancel(self) -> Dict[str, Any]:
self.status = workload_rsp.get("status", "CANCELED")
return workload_rsp

@validator("result", always=True)
@field_validator("result")
def result_link_to_result(
cls, result: Optional[Dict[str, Any]], values: Dict[str, Any]
cls, result: Optional[Dict[str, Any]], info: ValidationInfo
) -> Optional[Dict[str, Any]]:
result_link: Optional[str] = values.get("result_link")
result_link: Optional[str] = info.data.get("result_link")
if result or not result_link:
return result
try:
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@
"auth0-python >= 3.23.1, <4.0.0",
"requests>=2.25.1, <3.0.0",
"pyjwt[crypto]>=2.5.0, <3.0.0",
"pydantic>=1.10, <2.0",
"pydantic >= 2.6.0, <3.0.0",
],
extras_require={
"dev": {
"black==23.3.0",
"flake8==6.0.0",
"isort==5.12.0",
"mypy==0.982",
"mypy==1.9.0",
Mildophin marked this conversation as resolved.
Show resolved Hide resolved
"pytest==7.4.0",
"pytest-cov==4.1.0",
"types-requests==2.31.0.1",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ def test_batch_instantiation_with_extra_field(self, batch):
without breaking compatibility for users with old versions of the SDK where
the field is not present in the Batch class.
"""
batch_dict = batch.dict() # Batch data expected by the SDK
batch_dict = batch.model_dump() # Batch data expected by the SDK
# We add an extra field to mimick the API exposing new values to the user
batch_dict["new_field"] = "any_value"

Expand Down
2 changes: 1 addition & 1 deletion tests/test_device_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def init_sdk(self):
@pytest.mark.usefixtures("mock_request")
def test_get_device_specs_success(self):
device_specs_dict = self.sdk.get_device_specs_dict()
assert type(device_specs_dict) == dict
assert isinstance(device_specs_dict, dict)
specs = device_specs_dict["FRESNEL"]
json.loads(specs)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def test_job_instantiation_with_extra_field(self, job):
without breaking compatibility for users with old versions of the SDK where
the field is not present in the Job class.
"""
job_dict = job.dict() # job data expected by the SDK
job_dict = job.model_dump() # job data expected by the SDK
# We add an extra field to mimick the API exposing new values to the user
job_dict["new_field"] = "any_value"

Expand Down
Loading
Loading