Skip to content
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
26 changes: 25 additions & 1 deletion doc/source/getting_started/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,28 @@ PyHPS requires these packages as dependencies:
* `PyJWT <https://pypi.org/project/PyJWT/>`_

.. LINKS AND REFERENCES
.. _pip: https://pypi.org/project/pip/
.. _pip: https://pypi.org/project/pip/


Compatibility with HPS releases
-------------------------------

The following table summarizes the compatibility between PyHPS versions and HPS releases.

+------------------------------+-------------------------------+-------------------------------+------------------------------+
| PyHPS version / HPS release | ``1.0.2`` | ``1.1.1`` | ``1.2.0`` |
+==============================+===============================+===============================+==============================+
| ``0.7.X`` | :octicon:`check-circle-fill` | :octicon:`check-circle-fill` | :octicon:`check-circle-fill` |
+------------------------------+-------------------------------+-------------------------------+------------------------------+
| ``0.8.X`` | :octicon:`check-circle-fill` | :octicon:`check-circle-fill` | :octicon:`check-circle-fill` |
+------------------------------+-------------------------------+-------------------------------+------------------------------+
| ``0.9.X`` | :octicon:`check-circle` | :octicon:`check-circle-fill` | :octicon:`check-circle-fill` |
+------------------------------+-------------------------------+-------------------------------+------------------------------+
| ``0.10.X`` | :octicon:`x` | :octicon:`x` | :octicon:`check-circle-fill` |
+------------------------------+-------------------------------+-------------------------------+------------------------------+

Legend:

- :octicon:`check-circle-fill` Compatible
- :octicon:`check-circle` Backward compatible (new features exposed in PyHPS may not be available in older HPS releases)
- :octicon:`x` Incompatible
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies = [
"backoff>=2.0.0",
"pydantic>=1.10.0",
"PyJWT>=2.8.0",
"ansys-hps-data-transfer-client@git+https://github.com/ansys-internal/hps-data-transfer-client.git@main#egg=ansys-hps-data-transfer-client"
"ansys-hps-data-transfer-client@git+https://github.com/ansys/pyhps-data-transfer.git@main"
]

[project.optional-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion src/ansys/hps/client/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@

# this is only a convenience to default the version
# of Ansys simulation applications in PyHPS examples
__ansys_apps_version__ = "2024 R1"
__ansys_apps_version__ = "2025 R1"
131 changes: 131 additions & 0 deletions src/ansys/hps/client/check_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""
Version compatibility checks.
"""

from enum import Enum
from functools import wraps
from typing import Protocol

from .exceptions import VersionCompatibilityError


class HpsRelease(Enum):
"""HPS release versions."""

v1_0_2 = "1.0.2"
v1_1_1 = "1.1.1"
v1_2_0 = "1.2.0"


"""HPS to JMS version mapping."""
JMS_VERSIONS: dict[HpsRelease, str] = {
HpsRelease.v1_0_2: "1.0.12",
HpsRelease.v1_1_1: "1.0.20",
HpsRelease.v1_2_0: "1.1.4",
}


"""HPS to RMS version mapping."""
RMS_VERSIONS: dict[HpsRelease, str] = {
HpsRelease.v1_0_2: "1.0.0",
HpsRelease.v1_1_1: "1.1.5",
HpsRelease.v1_2_0: "1.1.10",
}


class ApiProtocol(Protocol):
"""Protocol for API classes."""

@property
def version(self) -> str:
pass


def check_min_version(version: str, min_version: str) -> bool:
"""Check if a version string meets a minimum version."""
from packaging.version import parse

return parse(version) >= parse(min_version)


def check_max_version(version: str, max_version: str) -> bool:
"""Check if a version string meets a maximum version."""
from packaging.version import parse

return parse(version) <= parse(max_version)


def check_min_version_and_raise(version, min_version: str, msg=None):
"""Check if a version meets a minimum version, raise an exception if not."""

if not check_min_version(version, min_version):

if msg is None:
msg = f"Version {version} is not supported. Minimum version required: {min_version}"
raise VersionCompatibilityError(msg)


def check_max_version_and_raise(version, max_version: str, msg=None):
"""Check if a version meets a maximum version, raise an exception if not."""

if not check_max_version(version, max_version):

if msg is None:
msg = f"Version {version} is not supported. Maximum version required: {max_version}"
raise VersionCompatibilityError(msg)


def check_version_and_raise(
version, min_version: str | None = None, max_version: str | None = None, msg=None
):
"""Check if a version meets the min/max requirements, raise an exception if not."""

if min_version is not None:
check_min_version_and_raise(version, min_version, msg)

if max_version is not None:
check_max_version_and_raise(version, max_version, msg)


def version_required(min_version=None, max_version=None):
"""Decorator for API methods to check version requirements."""

def decorator(func):

@wraps(func)
def wrapper(self: ApiProtocol, *args, **kwargs):
if min_version is not None:
msg = f"{func.__name__} requires {type(self).__name__} version >= " + min_version
check_min_version_and_raise(self.version, min_version, msg)

if max_version is not None:
msg = f"{func.__name__} requires {type(self).__name__} version <= " + max_version
check_max_version_and_raise(self.version, max_version, msg)

return func(self, *args, **kwargs)

return wrapper

return decorator
43 changes: 30 additions & 13 deletions src/ansys/hps/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from typing import Union
import warnings

from ansys.hps.data_transfer.client import Client as DTClient
from ansys.hps.data_transfer.client import Client as DataTransferClient
from ansys.hps.data_transfer.client import DataTransferApi
import jwt
import requests
Expand Down Expand Up @@ -163,7 +163,9 @@ def __init__(
self.client_secret = client_secret
self.verify = verify
self.data_transfer_url = url + f"/dt/api/v1"
self.dt_client = None

self._dt_client: DataTransferClient | None = None
self._dt_api: DataTransferApi | None = None

if self.verify is None:
self.verify = False
Expand Down Expand Up @@ -265,9 +267,9 @@ def __init__(
self._unauthorized_max_retry = 1

def exit_handler():
if self.dt_client is not None:
if self._dt_client is not None:
log.info("Stopping the data transfer client gracefully.")
self.dt_client.stop()
self._dt_client.stop()

atexit.register(exit_handler)

Expand Down Expand Up @@ -295,25 +297,26 @@ def rep_url(self) -> str:
log.warning(msg)
return self.url

def _start_dt_worker(self):
def initialize_data_transfer_client(self):
"""Initialize the Data Transfer client."""

if self.dt_client is None:
if self._dt_client is None:
try:
log.info("Starting Data Transfer client.")
# start Data transfer client
self.dt_client = DTClient(download_dir=self._get_download_dir("Ansys"))
self._dt_client = DataTransferClient(download_dir=self._get_download_dir("Ansys"))

self.dt_client.binary_config.update(
self._dt_client.binary_config.update(
verbosity=3,
debug=False,
insecure=True,
token=self.access_token,
data_transfer_url=self.data_transfer_url,
)
self.dt_client.start()
self._dt_client.start()

self.dt_api = DataTransferApi(self.dt_client)
self.dt_api.status(wait=True)
self._dt_api = DataTransferApi(self._dt_client)
self._dt_api.status(wait=True)
except Exception as ex:
log.debug(ex)
raise HPSError("Error occurred when starting Data Transfer client.")
Expand Down Expand Up @@ -377,8 +380,8 @@ def _auto_refresh_token(self, response, *args, **kwargs):
response.request.headers.update(
{"Authorization": self.session.headers["Authorization"]}
)
if self.dt_client is not None:
self.dt_client.binary_config.update(token=self.access_token)
if self._dt_client is not None:
self._dt_client.binary_config.update(token=self.access_token)
log.debug(f"Retrying request with updated access token.")
return self.session.send(response.request)

Expand Down Expand Up @@ -413,3 +416,17 @@ def refresh_access_token(self):
self.access_token = tokens["access_token"]
self.refresh_token = tokens.get("refresh_token", None)
self.session.headers.update({"Authorization": "Bearer %s" % tokens["access_token"]})

@property
def data_transfer_client(self) -> DataTransferClient:
"""Data Transfer client. If the client is not initialized, it will be started."""
if self._dt_client is None:
self.initialize_data_transfer_client()
return self._dt_client

@property
def data_transfer_api(self) -> DataTransferApi:
"""Data Transfer API. If the client is not initialized, it will be started."""
if self._dt_client is None:
self.initialize_data_transfer_client()
return self._dt_api
7 changes: 7 additions & 0 deletions src/ansys/hps/client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ def __init__(self, *args, **kwargs):
super(ClientError, self).__init__(*args, **kwargs)


class VersionCompatibilityError(ClientError):
"""Provides version compatibility errors."""

def __init__(self, *args, **kwargs):
super(VersionCompatibilityError, self).__init__(*args, **kwargs)


def raise_for_status(response, *args, **kwargs):
"""Automatically checks HTTP errors.

Expand Down
32 changes: 19 additions & 13 deletions src/ansys/hps/client/jms/api/jms_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from ansys.hps.data_transfer.client.models.msg import SrcDst, StoragePath
from ansys.hps.data_transfer.client.models.ops import OperationState

"""Module wrapping around the JMS root endpoints."""

from functools import cache
import json
import logging
import os
from typing import Dict, List, Union

from ansys.hps.data_transfer.client.models.msg import SrcDst, StoragePath
from ansys.hps.data_transfer.client.models.ops import OperationState
import backoff

from ansys.hps.client.check_version import JMS_VERSIONS, HpsRelease, version_required
from ansys.hps.client.client import Client
from ansys.hps.client.common import Object
from ansys.hps.client.exceptions import HPSError
Expand All @@ -42,7 +45,7 @@
log = logging.getLogger(__name__)


class JmsApi(object):
class JmsApi:
"""Wraps around the JMS root endpoints.

Parameters
Expand All @@ -68,13 +71,13 @@ class JmsApi(object):
def __init__(self, client: Client):
"""Initialize JMS API."""
self.client = client
self._fs_url = None

@property
def url(self) -> str:
"""URL of the API."""
return f"{self.client.url}/jms/api/v1"

@cache
def get_api_info(self):
"""Get information of the JMS API that the client is connected to.

Expand All @@ -83,6 +86,11 @@ def get_api_info(self):
r = self.client.session.get(self.url)
return r.json()

@property
def version(self) -> str:
"""API version."""
return self.get_api_info()["build"]["version"]

################################################################
# Projects
def get_projects(self, as_objects=True, **query_params) -> List[Project]:
Expand Down Expand Up @@ -121,6 +129,7 @@ def delete_project(self, project):
"""Delete a project."""
return delete_project(self.client, self.url, project)

@version_required(min_version=JMS_VERSIONS[HpsRelease.v1_2_0])
def restore_project(self, path: str) -> Project:
"""Restore a project from an archive.

Expand Down Expand Up @@ -442,26 +451,23 @@ def _restore_project(jms_api, archive_path):

# Delete archive file on server
log.info(f"Delete temporary bucket {bucket}")
op = jms_api.client.dt_api.rmdir([StoragePath(path=bucket)])
op = jms_api.client.dt_api.wait_for([op.id])
op = jms_api.client.data_transfer_api.rmdir([StoragePath(path=bucket)])
op = jms_api.client.data_transfer_api.wait_for([op.id])
if op[0].state != OperationState.Succeeded:
raise HPSError(f"Delete temporary bucket {bucket} failed")

return get_project(jms_api.client, jms_api.url, project_id)


def _upload_archive(jms_api: JmsApi, archive_path, bucket):
"""
Uploads archive using data transfer worker.

"""
jms_api.client._start_dt_worker()
"""Uploads archive using data transfer worker."""
jms_api.client.initialize_data_transfer_client()

src = StoragePath(path=archive_path, remote="local")
dst = StoragePath(path=f"{bucket}/{os.path.basename(archive_path)}")

op = jms_api.client.dt_api.copy([SrcDst(src=src, dst=dst)])
op = jms_api.client.dt_api.wait_for(op.id)
op = jms_api.client.data_transfer_api.copy([SrcDst(src=src, dst=dst)])
op = jms_api.client.data_transfer_api.wait_for(op.id)

log.info(f"Operation {op[0].state}")
if op[0].state != OperationState.Succeeded:
Expand Down
Loading
Loading