diff --git a/.gitignore b/.gitignore index 5410e8234..88ee486f4 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,8 @@ cython_debug/ #.idea/ # End of https://www.toptal.com/developers/gitignore/api/python + +doc/source/api/schemas +test_results.xml +mapdl_motorbike_frame.zip +examples/mapdl_motorbike_frame/task_input_file* \ No newline at end of file diff --git a/ansys/rep/client/__init__.py b/ansys/rep/client/__init__.py index 1f0f140f3..e3240e648 100644 --- a/ansys/rep/client/__init__.py +++ b/ansys/rep/client/__init__.py @@ -14,4 +14,5 @@ __version__, __version_no_dots__, ) +from .client import Client from .exceptions import APIError, ClientError, REPError diff --git a/ansys/rep/client/auth/__init__.py b/ansys/rep/client/auth/__init__.py index 4c2e1b0bc..a1850e32d 100644 --- a/ansys/rep/client/auth/__init__.py +++ b/ansys/rep/client/auth/__init__.py @@ -5,6 +5,6 @@ # # Author(s): F.Negri O.Koenig # ---------------------------------------------------------- +from .api import AuthApi from .authenticate import authenticate -from .client import Client from .resource import User diff --git a/ansys/rep/client/auth/api/__init__.py b/ansys/rep/client/auth/api/__init__.py new file mode 100644 index 000000000..2d0324109 --- /dev/null +++ b/ansys/rep/client/auth/api/__init__.py @@ -0,0 +1 @@ +from .auth_api import AuthApi diff --git a/ansys/rep/client/auth/client.py b/ansys/rep/client/auth/api/auth_api.py similarity index 56% rename from ansys/rep/client/auth/client.py rename to ansys/rep/client/auth/api/auth_api.py index 03cca9214..f5b743ba2 100644 --- a/ansys/rep/client/auth/client.py +++ b/ansys/rep/client/auth/api/auth_api.py @@ -6,14 +6,10 @@ # Author(s): F.Negri, O.Koenig # ---------------------------------------------------------- -from ansys.rep.client.connection import create_session +from ..resource.user import create_user, delete_user, get_users, update_user -from ..exceptions import raise_for_status -from .authenticate import authenticate -from .resource.user import create_user, delete_user, get_users, update_user - -class Client(object): +class AuthApi: """A python interface to the Authorization Service API. Users with admin rights (such as the default ``repadmin`` user) can create new @@ -37,47 +33,54 @@ class Client(object): """ - def __init__( - self, - rep_url, - *, - realm: str = "rep", - username: str = "repadmin", - password: str = "repadmin", - grant_type: str = "password", - scope="openid", - client_id: str = "rep-cli", - client_secret: str = None, - ): - - self.rep_url = rep_url - self.auth_api_url = self.rep_url + f"/auth/" - - self.username = username - self.password = password - self.realm = realm - self.grant_type = grant_type - self.scope = scope - self.client_id = client_id - self.client_secret = client_secret - - tokens = authenticate( - url=self.rep_url, - realm=realm, - grant_type=grant_type, - scope=scope, - client_id=client_id, - client_secret=client_secret, - username=username, - password=password, - ) - self.access_token = tokens["access_token"] - - self.session = create_session(self.access_token) - self.session.headers["content-type"] = "application/json" - - # register hook to handle expiring of the refresh token - self.session.hooks["response"] = [raise_for_status] + def __init__(self, client): + self.client = client + + @property + def url(self): + return f"{self.client.rep_url}/auth/" + + # def __init__( + # self, + # rep_url, + # *, + # realm: str = "rep", + # username: str = "repadmin", + # password: str = "repadmin", + # grant_type: str = "password", + # scope="openid", + # client_id: str = "rep-cli", + # client_secret: str = None, + # ): + + # self.rep_url = rep_url + # self.auth_api_url = self.rep_url + f"/auth/" + + # self.username = username + # self.password = password + # self.realm = realm + # self.grant_type = grant_type + # self.scope = scope + # self.client_id = client_id + # self.client_secret = client_secret + + # tokens = authenticate( + # url=self.rep_url, + # realm=realm, + # grant_type=grant_type, + # scope=scope, + # client_id=client_id, + # client_secret=client_secret, + # username=username, + # password=password, + # ) + # self.access_token = tokens["access_token"] + + # self.session = create_session(self.access_token) + # self.session.headers["content-type"] = "application/json" + + # # register hook to handle expiring of the refresh token + # self.session.hooks["response"] = [raise_for_status] # def get_api_info(self): # """Return info like version, build date etc of the Auth API the client is connected to.""" @@ -86,7 +89,7 @@ def __init__( def get_users(self, as_objects=True): """Return a list of users.""" - return get_users(self, as_objects=as_objects) + return get_users(self.client, as_objects=as_objects) def create_user(self, user, as_objects=True): """Create a new user. @@ -95,7 +98,7 @@ def create_user(self, user, as_objects=True): user (:class:`ansys.rep.client.auth.User`): A User object. Defaults to None. as_objects (bool, optional): Defaults to True. """ - return create_user(self, user, as_objects=as_objects) + return create_user(self.client, user, as_objects=as_objects) def update_user(self, user, as_objects=True): """Modify an existing user. @@ -112,4 +115,4 @@ def delete_user(self, user): Args: user (:class:`ansys.rep.client.auth.User`): A User object. Defaults to None. """ - return delete_user(self, user) + return delete_user(self.client, user) diff --git a/ansys/rep/client/auth/resource/user.py b/ansys/rep/client/auth/resource/user.py index 914bccbee..2be397758 100644 --- a/ansys/rep/client/auth/resource/user.py +++ b/ansys/rep/client/auth/resource/user.py @@ -45,12 +45,14 @@ def __init__(self, **kwargs): def _admin_client(client): + raise NotImplementedError("KeycloakAdmin currently doesn't support a token auth workflow. TODO") keycloak_admin = KeycloakAdmin( server_url=client.auth_api_url, - username=client.username, - password=client.password, + username=None, + password=None, realm_name=client.realm, - client_secret_key=client.client_secret, + # refresh_token=client.refresh_token, + # access_token=client.access_token, client_id=client.client_id, verify=False, ) diff --git a/ansys/rep/client/client.py b/ansys/rep/client/client.py new file mode 100644 index 000000000..af3c46e51 --- /dev/null +++ b/ansys/rep/client/client.py @@ -0,0 +1,120 @@ +# ---------------------------------------------------------- +# Copyright (C) 2019 by +# ANSYS Switzerland GmbH +# www.ansys.com +# +# Author(s): F.Negri, O.Koenig +# ---------------------------------------------------------- + +from .auth.authenticate import authenticate +from .connection import create_session +from .exceptions import raise_for_status + + +class Client(object): + """A python interface to the Remote Execution Platform (REP) API. + + Uses the provided credentials to create and store + an authorized :class:`requests.Session` object. + + The following authentication workflows are supported: + + - Username and password: the client connects to the OAuth server and + requests access and refresh tokens. + - Refresh token: the client connects to the OAuth server and + requests a new access token. + - Access token: no authentication needed. + + Args: + rep_url (str): The base path for the server to call, + e.g. "https://127.0.0.1/dcs". + username (str): Username (Optional) + password (str): Password (Optional) + refresh_token (str): Refresh Token (Optional) + access_token (str): Access Token (Optional) + + Example: + + >>> from ansys.rep.client.jms import Client + >>> # Create client object and connect to DCS with username & password + >>> cl = Client(rep_url="https://127.0.0.1/dcs", username="dcadmin", password="dcadmin") + >>> # Extract refresh token to eventually store it + >>> refresh_token = cl.refresh_token + >>> # Alternative: Create client object and connect to DCS with refresh token + >>> cl = Client(rep_url="https://127.0.0.1/dcs", refresh_token=refresh_token) + + """ + + def __init__( + self, + rep_url: str = "https://127.0.0.1:8443/rep", + username: str = "repadmin", + password: str = "repadmin", + *, + realm: str = "rep", + grant_type: str = "password", + scope="openid", + client_id: str = "rep-cli", + client_secret: str = None, + access_token: str = None, + refresh_token: str = None, + auth_url: str = None, + ): + + self.rep_url = rep_url + self.auth_url = auth_url + self.auth_api_url = (auth_url or rep_url) + f"/auth/" + self.refresh_token = None + self.username = username + self.realm = realm + self.grant_type = grant_type + self.scope = scope + self.client_id = client_id + self.client_secret = client_secret + + if access_token: + self.access_token = access_token + else: + tokens = authenticate( + url=auth_url or rep_url, + realm=realm, + grant_type=grant_type, + scope=scope, + client_id=client_id, + client_secret=client_secret, + username=username, + password=password, + refresh_token=refresh_token, + ) + self.access_token = tokens["access_token"] + self.refresh_token = tokens["refresh_token"] + + self.session = create_session(self.access_token) + # register hook to handle expiring of the refresh token + self.session.hooks["response"] = [self._auto_refresh_token, raise_for_status] + self._unauthorized_num_retry = 0 + self._unauthorized_max_retry = 1 + + def _auto_refresh_token(self, response, *args, **kwargs): + """Hook function to automatically refresh the access token and + re-send the request in case of unauthorized error""" + if ( + response.status_code == 401 + and self._unauthorized_num_retry < self._unauthorized_max_retry + ): + self._unauthorized_num_retry += 1 + self.refresh_access_token() + response.request.headers.update( + {"Authorization": self.session.headers["Authorization"]} + ) + return self.session.send(response.request) + + self._unauthorized_num_retry = 0 + return response + + def refresh_access_token(self): + """Use the refresh token to obtain a new access token""" + tokens = authenticate(url=self.auth_url or self.rep_url, refresh_token=self.refresh_token) + self.access_token = tokens["access_token"] + self.refresh_token = tokens["refresh_token"] + self.session.headers.update({"Authorization": "Bearer %s" % tokens["access_token"]}) diff --git a/ansys/rep/client/jms/__init__.py b/ansys/rep/client/jms/__init__.py index f280999dd..90dd09c40 100644 --- a/ansys/rep/client/jms/__init__.py +++ b/ansys/rep/client/jms/__init__.py @@ -6,7 +6,7 @@ # Author(s): O.Koenig # ---------------------------------------------------------- -from .client import Client +from .api import JmsApi, ProjectApi from .resource import ( Algorithm, BoolParameterDefinition, @@ -18,12 +18,12 @@ IntParameterDefinition, Job, JobDefinition, + JobSelection, Licensing, ParameterMapping, Project, ProjectPermission, ResourceRequirements, - Selection, Software, StringParameterDefinition, SuccessCriteria, diff --git a/ansys/rep/client/jms/api/__init__.py b/ansys/rep/client/jms/api/__init__.py new file mode 100644 index 000000000..c1833923e --- /dev/null +++ b/ansys/rep/client/jms/api/__init__.py @@ -0,0 +1,2 @@ +from .jms_api import JmsApi +from .project_api import ProjectApi diff --git a/ansys/rep/client/jms/api/base.py b/ansys/rep/client/jms/api/base.py new file mode 100644 index 000000000..f389fcf72 --- /dev/null +++ b/ansys/rep/client/jms/api/base.py @@ -0,0 +1,123 @@ +import json +import logging +from typing import List + +from requests import Session + +from ansys.rep.client.exceptions import ClientError + +from ..resource.base import Object + +log = logging.getLogger(__name__) + + +def get_objects(session: Session, url: str, obj_type: Object, as_objects=True, **query_params): + + rest_name = obj_type.Meta.rest_name + url = f"{url}/{rest_name}" + query_params.setdefault("fields", "all") + r = session.get(url, params=query_params) + + if query_params.get("count"): + return r.json()[f"num_{rest_name}"] + + data = r.json()[rest_name] + if not as_objects: + return data + + schema = obj_type.Meta.schema(many=True) + return schema.load(data) + + +def get_object( + session: Session, url: str, obj_type: Object, id: str, as_object=True, **query_params +): + + rest_name = obj_type.Meta.rest_name + url = f"{url}/{rest_name}/{id}" + query_params.setdefault("fields", "all") + r = session.get(url, params=query_params) + + data = r.json()[rest_name] + if not as_object: + return data + + schema = obj_type.Meta.schema(many=True) + if len(data) == 0: + return None + elif len(data) == 1: + return schema.load(data)[0] + elif len(data) > 1: + raise ClientError( + f"Multiple {Object.__class__.__name__} objects with id={id}: {schema.load(data)}" + ) + + +def create_objects( + session: Session, url: str, objects: List[Object], as_objects=True, **query_params +): + if not objects: + return [] + + are_same = [o.__class__ == objects[0].__class__ for o in objects[1:]] + if not all(are_same): + raise ClientError("Mixed object types") + + obj_type = objects[0].__class__ + rest_name = obj_type.Meta.rest_name + + url = f"{url}/{rest_name}" + query_params.setdefault("fields", "all") + schema = obj_type.Meta.schema(many=True) + serialized_data = schema.dump(objects) + json_data = json.dumps({rest_name: serialized_data}) + + r = session.post(f"{url}", data=json_data, params=query_params) + data = r.json()[rest_name] + if not as_objects: + return data + + return schema.load(data) + + +def update_objects( + session: Session, url: str, objects: List[Object], as_objects=True, **query_params +): + if not objects: + return [] + + are_same = [o.__class__ == objects[0].__class__ for o in objects[1:]] + if not all(are_same): + raise ClientError("Mixed object types") + + obj_type = objects[0].__class__ + rest_name = obj_type.Meta.rest_name + + url = f"{url}/{rest_name}" + query_params.setdefault("fields", "all") + schema = obj_type.Meta.schema(many=True) + serialized_data = schema.dump(objects) + json_data = json.dumps({rest_name: serialized_data}) + r = session.put(f"{url}", data=json_data, params=query_params) + + data = r.json()[rest_name] + if not as_objects: + return data + + return schema.load(data) + + +def delete_objects(session: Session, url: str, objects: List[Object]): + if not objects: + return + + are_same = [o.__class__ == objects[0].__class__ for o in objects[1:]] + if not all(are_same): + raise ClientError("Mixed object types") + + obj_type = objects[0].__class__ + rest_name = obj_type.Meta.rest_name + url = f"{url}/{rest_name}" + data = json.dumps({"source_ids": [obj.id for obj in objects]}) + + r = session.delete(url, data=data) diff --git a/ansys/rep/client/jms/api/jms_api.py b/ansys/rep/client/jms/api/jms_api.py new file mode 100644 index 000000000..f434efc97 --- /dev/null +++ b/ansys/rep/client/jms/api/jms_api.py @@ -0,0 +1,270 @@ +import json +import logging +import os +import time +from typing import List +import uuid + +from ansys.rep.client.exceptions import REPError + +from ..resource import Operation +from ..resource.evaluator import Evaluator +from ..resource.project import Project, ProjectSchema +from ..resource.task_definition_template import TaskDefinitionTemplate +from .base import create_objects, delete_objects, get_object, get_objects, update_objects + +log = logging.getLogger(__name__) + + +class JmsApi(object): + """Root API + + Args: + client (:class:`ansys.rep.client.jms.Client`): A client object. + + Example: + + >>> from ansys.rep.client.jms import Client + >>> # Create client object and connect to REP with username & password + >>> cl = Client(rep_url="https://127.0.0.1/dcs", username="repadmin", password="repadmin") + >>> # Create a new project + >>> root_api = RootApi(client) + >>> project = root_api.create_project(Project(name="Example Project")) + + """ + + def __init__(self, client): + self.client = client + + @property + def url(self): + return f"{self.client.rep_url}/jms/api/v1" + + def get_api_info(self): + """Return info like version, build date etc of the JMS API the client is connected to""" + r = self.client.session.get(self.url) + return r.json() + + ################################################################ + # Projects + def get_projects(self, **query_params): + """Return a list of projects, optionally filtered by given query parameters""" + return get_projects(self.client, self.url, **query_params) + + def get_project(self, id=None, name=None): + """Return a single project for given project id""" + return get_project(self.client, self.url, id, name) + + def create_project(self, project, replace=False, as_objects=True): + """Create a new project""" + return create_project(self.client, self.url, project, replace, as_objects) + + def update_project(self, project, as_objects=True): + """Update an existing project""" + return update_project(self.client, self.url, project, as_objects) + + def delete_project(self, project): + """Delete a project""" + return delete_project(self.client, self.url, project) + + def restore_project(self, path: str) -> Project: + """Restore a project from an archive + + Args: + path (str): Path of the archive file to be restored. + project_name (str): Name of the restored project. (optional) + + Returns: + :class:`ansys.rep.client.jms.Project`: A Project object. + """ + return restore_project(self, path) + + ################################################################ + # Evaluators + def get_evaluators(self, as_objects=True, **query_params): + """Return a list of evaluators, optionally filtered by given query parameters""" + return get_objects(self.client.session, self.url, Evaluator, as_objects, **query_params) + + def update_evaluators(self, evaluators, as_objects=True, **query_params): + """Update evaluators configuration""" + return update_objects(self.client.session, self.url, evaluators, as_objects, **query_params) + + ################################################################ + # Task Definition Templates + def get_task_definition_templates(self, as_objects=True, **query_params): + """Return a list of task definition templates, + optionally filtered by given query parameters""" + return get_objects( + self.client.session, self.url, TaskDefinitionTemplate, as_objects, **query_params + ) + + def create_task_definition_templates(self, templates, as_objects=True, **query_params): + """Create new task definition templates + + Args: + templates (list of :class:`ansys.rep.client.jms.TaskDefinitionTemplate`): + A list of task definition templates + """ + return create_objects(self.client.session, self.url, templates, as_objects, **query_params) + + def update_task_definition_templates(self, templates, as_objects=True, **query_params): + """Update existing task definition templates + + Args: + templates (list of :class:`ansys.rep.client.jms.TaskDefinitionTemplate`): + A list of task definition templates + """ + return update_objects(self.client.session, self.url, templates, as_objects, **query_params) + + def delete_task_definition_templates(self, templates): + """Delete existing task definition templates + + Args: + templates (list of :class:`ansys.rep.client.jms.TaskDefinitionTemplate`): + A list of task definition templates + """ + return delete_objects(self.client.session, self.url, templates) + + ################################################################ + # Operations + def get_operations(self, as_objects=True, **query_params): + return get_objects( + self.client.session, self.url, Operation, as_objects=as_objects, **query_params + ) + + def get_operation(self, id, as_object=True): + return get_object(self.client.session, self.url, Operation, id, as_object=as_object) + + def _monitor_operation(self, operation_id: str, interval: float = 1.0): + return _monitor_operation(self, operation_id, interval) + + +def get_projects(client, api_url, as_objects=True, **query_params) -> List[Project]: + """ + Returns list of projects + """ + url = f"{api_url}/projects" + r = client.session.get(url, params=query_params) + + data = r.json()["projects"] + if not as_objects: + return data + + schema = ProjectSchema(many=True) + return schema.load(data) + + +def get_project(client, api_url, id=None, name=None) -> Project: + """ + Return a single project + """ + params = {} + if name: + url = f"{api_url}/projects/" + params["name"] = name + else: + url = f"{api_url}/projects/{id}" + + r = client.session.get(url, params=params) + + if len(r.json()["projects"]): + schema = ProjectSchema() + return schema.load(r.json()["projects"][0]) + return None + + +def create_project(client, api_url, project, replace=False, as_objects=True) -> Project: + url = f"{api_url}/projects/" + + schema = ProjectSchema() + serialized_data = schema.dump(project) + json_data = json.dumps({"projects": [serialized_data], "replace": replace}) + r = client.session.post(f"{url}", data=json_data) + + data = r.json()["projects"][0] + if not as_objects: + return data + + return schema.load(data) + + +def update_project(client, api_url, project, as_objects=True) -> Project: + url = f"{api_url}/projects/{project.id}" + + schema = ProjectSchema() + serialized_data = schema.dump(project) + json_data = json.dumps({"projects": [serialized_data]}) + r = client.session.put(f"{url}", data=json_data) + + data = r.json()["projects"][0] + if not as_objects: + return data + + return schema.load(data) + + +def delete_project(client, api_url, project): + + url = f"{api_url}/projects/{project.id}" + r = client.session.delete(url) + + +def _monitor_operation(jms_api: JmsApi, operation_id: str, interval: float = 1.0): + + done = False + op = None + while not done: + op = jms_api.get_operation(id=operation_id) + if op: + done = op.finished + log.info( + f"Operation {op.name} - progress={op.progress * 100.0}%, " + f"succeeded={op.succeeded}, finished={op.finished}" + ) + time.sleep(interval) + return op + + +def restore_project(jms_api, archive_path): + + if not os.path.exists(archive_path): + raise REPError(f"Project archive: path does not exist {archive_path}") + + # Upload archive to FS API + archive_name = os.path.basename(archive_path) + + bucket = f"rep-client-restore-{uuid.uuid4()}" + fs_file_url = f"{jms_api.client.rep_url}/fs/api/v1/{bucket}/{archive_name}" + ansfs_file_url = f"ansfs://{bucket}/{archive_name}" + + fs_headers = {"content-type": "application/octet-stream"} + + log.info(f"Uploading archive to {fs_file_url}") + with open(archive_path, "rb") as file_content: + r = jms_api.client.session.post(fs_file_url, data=file_content, headers=fs_headers) + + # POST restore request + log.info(f"Restoring archive from {ansfs_file_url}") + url = f"{jms_api.url}/projects/archive" + query_params = {"backend_path": ansfs_file_url} + r = jms_api.client.session.post(url, params=query_params) + + # Monitor restore operation + operation_location = r.headers["location"] + log.debug(f"Operation location: {operation_location}") + operation_id = operation_location.rsplit("/", 1)[-1] + log.debug(f"Operation id: {operation_id}") + + op = _monitor_operation(jms_api, operation_id, 1.0) + + if not op.succeeded: + raise REPError(f"Failed to restore project from archive {archive_path}.") + + project_id = op.result["project_id"] + log.info(f"Done restoring project, project_id = '{project_id}'") + + # Delete archive file on server + log.info(f"Delete temporary bucket {bucket}") + r = jms_api.client.session.put(f"{jms_api.client.rep_url}/fs/api/v1/remove/{bucket}") + + return get_project(jms_api.client, jms_api.url, project_id) diff --git a/ansys/rep/client/jms/api/project_api.py b/ansys/rep/client/jms/api/project_api.py new file mode 100644 index 000000000..73db66fa3 --- /dev/null +++ b/ansys/rep/client/jms/api/project_api.py @@ -0,0 +1,481 @@ +import logging +import os +from pathlib import Path +from typing import Callable, List + +from ansys.rep.client.exceptions import ClientError, REPError + +from ..resource.algorithm import Algorithm +from ..resource.base import Object +from ..resource.file import File +from ..resource.job import Job, copy_jobs, sync_jobs +from ..resource.job_definition import JobDefinition +from ..resource.license_context import LicenseContext +from ..resource.parameter_definition import ParameterDefinition +from ..resource.parameter_mapping import ParameterMapping +from ..resource.project import get_fs_url +from ..resource.project_permission import ProjectPermission, update_permissions +from ..resource.selection import JobSelection +from ..resource.task import Task +from ..resource.task_definition import TaskDefinition +from .base import create_objects, delete_objects, get_objects, update_objects +from .jms_api import JmsApi, _monitor_operation, get_project + +log = logging.getLogger(__name__) + + +class ProjectApi: + """Project API + + Args: + client (:class:`ansys.rep.client.jms.Client`): A client object. + project_id (str): + + Example: + + >>> project = Project(name="Example Project") + >>> project = client.create_project(project) + >>> project_api = ProjectApi(client, project.id) + >>> jobs = project_api.get_jobs() + + """ + + def __init__(self, client, project_id: str): + self.client = client + self.project_id = project_id + self._fs_url = None + self._fs_project_id = None + + @property + def jms_api_url(self): + return f"{self.client.rep_url}/jms/api/v1" + + @property + def url(self): + return f"{self.jms_api_url}/projects/{self.project_id}" + + @property + def fs_url(self): + """URL of the file storage gateway""" + if self._fs_url is None or self._fs_project_id != self.project_id: + self._fs_project_id = self.project_id + project = get_project(self.client, self.jms_api_url, id=self.project_id) + self._fs_url = get_fs_url(project) + return self._fs_url + + @property + def fs_bucket_url(self): + """URL of the project's bucket in the file storage gateway""" + return f"{self.fs_url}/{self.project_id}" + + ################################################################ + # Project operations (copy, archive) + def copy_project(self, project_target_name, wait=True): + """Duplicate project""" + return copy_project(self, self.project_id, project_target_name, wait) + + def archive_project(self, path, include_job_files=True): + """Archive an existing project and save it to disk + + Args: + path (str): Where to save the archive locally. + include_job_files (bool, optional): Whether to include design point files in the + archive. True by default. + + Returns: + str: The path to the archive. + """ + return archive_project(self, path, include_job_files) + + ################################################################ + # Files + def get_files(self, as_objects=True, content=False, **query_params): + """ + Return a list of file resources, optionally filtered by given query parameters. + If content=True, each files content is downloaded as well and stored in memory + as :attr:`ansys.rep.client.jms.File.content`. + """ + return get_files(self, as_objects=as_objects, content=content, **query_params) + + def create_files(self, files, as_objects=True): + return create_files(self, files, as_objects=as_objects) + + def update_files(self, files, as_objects=True): + return update_files(self, files, as_objects=as_objects) + + def delete_files(self, files): + return self._delete_objects(files) + + def download_file( + self, + file: File, + target_path: str, + stream: bool = True, + progress_handler: Callable[[int], None] = None, + ) -> str: + """ + Download file content and save it to disk. If stream=True, + data is retrieved in chunks, avoiding storing the entire content + in memory. + """ + return _download_file(self, file, target_path, progress_handler, stream) + + ################################################################ + # Parameter definitions + def get_parameter_definitions(self, as_objects=True, **query_params): + return self._get_objects(ParameterDefinition, as_objects, **query_params) + + def create_parameter_definitions(self, parameter_definitions, as_objects=True): + return self._create_objects(parameter_definitions, as_objects) + + def update_parameter_definitions(self, parameter_definitions, as_objects=True): + return self._update_objects(parameter_definitions, as_objects) + + def delete_parameter_definitions(self, parameter_definitions): + return self._delete_objects(parameter_definitions) + + ################################################################ + # Parameter mappings + def get_parameter_mappings(self, as_objects=True, **query_params): + return self._get_objects(ParameterMapping, as_objects=as_objects, **query_params) + + def create_parameter_mappings(self, parameter_mappings, as_objects=True): + return self._create_objects(parameter_mappings, as_objects=as_objects) + + def update_parameter_mappings(self, parameter_mappings, as_objects=True): + return self._update_objects(parameter_mappings, as_objects=as_objects) + + def delete_parameter_mappings(self, parameter_mappings): + return self._delete_objects(parameter_mappings) + + ################################################################ + # Task definitions + def get_task_definitions(self, as_objects=True, **query_params): + return self._get_objects(TaskDefinition, as_objects=as_objects, **query_params) + + def create_task_definitions(self, task_definitions, as_objects=True): + return self._create_objects(task_definitions, as_objects=as_objects) + + def update_task_definitions(self, task_definitions, as_objects=True): + return self._update_objects(task_definitions, as_objects=as_objects) + + def delete_task_definitions(self, task_definitions): + return self._delete_objects(task_definitions) + + ################################################################ + # Job definitions + def get_job_definitions(self, as_objects=True, **query_params): + return self._get_objects(JobDefinition, as_objects=as_objects, **query_params) + + def create_job_definitions(self, job_definitions, as_objects=True): + return self._create_objects(job_definitions, as_objects=as_objects) + + def update_job_definitions(self, job_definitions, as_objects=True): + return self._update_objects(job_definitions, as_objects=as_objects) + + def delete_job_definitions(self, job_definitions): + return self._delete_objects(job_definitions) + + ################################################################ + # Jobs + def get_jobs(self, as_objects=True, **query_params): + return self._get_objects(Job, as_objects=as_objects, **query_params) + + def create_jobs(self, jobs, as_objects=True): + """Create new jobs + + Args: + jobs (list of :class:`ansys.rep.client.jms.Job`): A list of Job objects + as_objects (bool): Whether to return jobs as objects or dictionaries + + Returns: + List of :class:`ansys.rep.client.jms.Job` or list of dict if `as_objects` is False + """ + return self._create_objects(jobs, as_objects=as_objects) + + def copy_jobs(self, jobs, as_objects=True, **query_params): + """Create new jobs by copying existing ones + + Args: + jobs (list of :class:`ansys.rep.client.jms.Job`): A list of job objects + + Note that only the ``id`` field of the Job objects need to be filled; + the other fields can be empty. + """ + return copy_jobs(self, jobs, as_objects=as_objects, **query_params) + + def update_jobs(self, jobs, as_objects=True): + """Update existing jobs + + Args: + jobs (list of :class:`ansys.rep.client.jms.Job`): A list of job objects + as_objects (bool): Whether to return jobs as objects or dictionaries + + Returns: + List of :class:`ansys.rep.client.jms.Job` or list of dict if `as_objects` is True + """ + return self._update_objects(jobs, as_objects=as_objects) + + def delete_jobs(self, jobs): + """Delete existing jobs + + Args: + jobs (list of :class:`ansys.rep.client.jms.Job`): A list of Job objects + + Note that only the ``id`` field of the Job objects need to be filled; + the other fields can be empty. + + Example: + + >>> dps_to_delete = [] + >>> for id in [1,2,39,44]: + >>> dps_to_delete.append(Job(id=id)) + >>> project_api.delete_jobs(dps_to_delete) + + """ + return self._delete_objects(jobs) + + def _sync_jobs(self, jobs): + return sync_jobs(self, jobs) + + ################################################################ + # Tasks + def get_tasks(self, as_objects=True, **query_params): + return self._get_objects(Task, as_objects=as_objects, **query_params) + + def update_tasks(self, tasks, as_objects=True): + return self._update_objects(tasks, as_objects=as_objects) + + ################################################################ + # Selections + def get_job_selections(self, as_objects=True, **query_params): + return self._get_objects(JobSelection, as_objects=as_objects, **query_params) + + def create_job_selections(self, selections, as_objects=True): + return self._create_objects(selections, as_objects=as_objects) + + def update_job_selections(self, selections, as_objects=True): + return self._update_objects(selections, as_objects=as_objects) + + def delete_job_selections(self, selections): + return self._delete_objects(selections) + + ################################################################ + # Algorithms + def get_algorithms(self, as_objects=True, **query_params): + return self._get_objects(Algorithm, as_objects=as_objects, **query_params) + + def create_algorithms(self, algorithms, as_objects=True): + return self._create_objects(algorithms, as_objects=as_objects) + + def update_algorithms(self, algorithms, as_objects=True): + return self._update_objects(algorithms, as_objects=as_objects) + + def delete_algorithms(self, algorithms): + return self._delete_objects(algorithms) + + ################################################################ + # Permissions + def get_permissions(self, as_objects=True): + return self._get_objects(ProjectPermission, as_objects=as_objects) + + def update_permissions(self, permissions): + # the rest api currently doesn't return anything on permissions update + update_permissions(self.client, self.url, permissions) + + ################################################################ + # License contexts + def get_license_contexts(self, as_objects=True, **query_params): + return self._get_objects(self, LicenseContext, as_objects=as_objects, **query_params) + + def create_license_contexts(self, as_objects=True): + rest_name = LicenseContext.Meta.rest_name + url = f"{self.jms_api_url}/projects/{self.id}/{rest_name}" + r = self.client.session.post(f"{url}") + data = r.json()[rest_name] + if not as_objects: + return data + schema = LicenseContext.Meta.schema(many=True) + objects = schema.load(data) + return objects + + def update_license_contexts(self, license_contexts, as_objects=True): + return self._update_objects(self, license_contexts, as_objects=as_objects) + + def delete_license_contexts(self): + rest_name = LicenseContext.Meta.rest_name + url = f"{self.jms_api_url}/projects/{self.id}/{rest_name}" + r = self.client.session.delete(url) + + ################################################################ + def _get_objects(self, obj_type: Object, as_objects=True, **query_params): + return get_objects(self.client.session, self.url, obj_type, as_objects, **query_params) + + def _create_objects(self, objects: List[Object], as_objects=True, **query_params): + return create_objects(self.client.session, self.url, objects, as_objects, **query_params) + + def _update_objects(self, objects: List[Object], as_objects=True, **query_params): + return update_objects(self.client.session, self.url, objects, as_objects, **query_params) + + def _delete_objects(self, objects: List[Object]): + delete_objects(self.client.session, self.url, objects) + + +def _download_files(project_api: ProjectApi, files: List[File]): + """ + Temporary implementation of file download directly using fs REST gateway. + To be replaced with direct ansft calls, when it is available as python pkg + """ + + for f in files: + if getattr(f, "hash", None) is not None: + r = project_api.client.session.get(f"{project_api.fs_bucket_url}/{f.storage_id}") + f.content = r.content + f.content_type = r.headers["Content-Type"] + + +def get_files(project_api: ProjectApi, as_objects=True, content=False, **query_params): + + files = get_objects( + project_api.client.session, project_api.url, File, as_objects=as_objects, **query_params + ) + if content: + _download_files(project_api, files) + return files + + +def _upload_files(project_api: ProjectApi, files): + """ + Temporary implementation of file upload directly using fs REST gateway. + To be replaced with direct ansft calls, when it is available as python pkg + """ + fs_headers = {"content-type": "application/octet-stream"} + + for f in files: + if getattr(f, "src", None) is not None: + with open(f.src, "rb") as file_content: + r = project_api.client.session.post( + f"{project_api.fs_bucket_url}/{f.storage_id}", + data=file_content, + headers=fs_headers, + ) + f.hash = r.json()["checksum"] + f.size = os.path.getsize(f.src) + + +def create_files(project_api: ProjectApi, files, as_objects=True) -> List[File]: + # (1) Create file resources in DPS + created_files = create_objects( + project_api.client.session, project_api.url, files, as_objects=as_objects + ) + + # (2) Check if there are src properties, files to upload + num_uploads = 0 + for f, cf in zip(files, created_files): + if getattr(f, "src", None) is not None: + cf.src = f.src + num_uploads += 1 + + if num_uploads > 0: + + # (3) Upload file contents + _upload_files(project_api, created_files) + + # (4) Update corresponding file resources in DPS with hashes of uploaded files + created_files = update_objects( + project_api.client.session, project_api.url, created_files, as_objects=as_objects + ) + + return created_files + + +def update_files(project_api: ProjectApi, files: List[File], as_objects=True) -> List[File]: + # Upload files first if there are any src parameters + _upload_files(project_api, files) + # Update file resources in DPS + return update_objects(project_api.client.session, project_api.url, files, as_objects=as_objects) + + +def _download_file( + project_api: ProjectApi, + file: File, + target_path: str, + progress_handler: Callable[[int], None] = None, + stream: bool = True, +) -> str: + + if getattr(file, "hash", None) is None: + raise ClientError(f"No hash found. Failed to download file {file.name}") + + Path(target_path).mkdir(parents=True, exist_ok=True) + download_link = f"{project_api.fs_bucket_url}/{file.storage_id}" + download_path = os.path.join(target_path, file.evaluation_path) + + with project_api.client.session.get(download_link, stream=stream) as r, open( + download_path, "wb" + ) as f: + for chunk in r.iter_content(chunk_size=None): + f.write(chunk) + if progress_handler is not None: + progress_handler(len(chunk)) + + return download_path + + +def copy_project(project_api: ProjectApi, project_source_id, project_target_name, wait=True) -> str: + + url = f"{project_api.url}/copy" + r = project_api.client.session.put(url, params={"project_name": project_target_name}) + + operation_location = r.headers["location"] + operation_id = operation_location.rsplit("/", 1)[-1] + + if not wait: + return operation_location + + op = _monitor_operation(JmsApi(project_api.client), operation_id, 1.0) + if not op.succeeded: + raise REPError(f"Failed to copy project {project_source_id}.") + return op.result + + +def archive_project(project_api: ProjectApi, target_path, include_job_files=True) -> str: + + # PUT archive request + url = f"{project_api.url}/archive" + query_params = {} + if not include_job_files: + query_params["download_files"] = "configuration" + + r = project_api.client.session.put(url, params=query_params) + + # Monitor archive operation + operation_location = r.headers["location"] + log.debug(f"Operation location: {operation_location}") + operation_id = operation_location.rsplit("/", 1)[-1] + + op = _monitor_operation(JmsApi(project_api.client), operation_id, 1.0) + + if not op.succeeded: + raise REPError(f"Failed to archive project {project_api.project_id}.\n{op}") + + download_link = op.result["backend_path"] + + # Download archive + download_link = download_link.replace("ansfs://", project_api.fs_url + "/") + log.info(f"Project archive download link: {download_link}") + + if not os.path.isdir(target_path): + raise REPError(f"Project archive: target path does not exist {target_path}") + + file_path = os.path.join(target_path, download_link.rsplit("/")[-1]) + log.info(f"Download archive to {file_path}") + + with project_api.client.session.get(download_link, stream=True) as r: + with open(file_path, "wb") as f: + for chunk in r.iter_content(chunk_size=1024 * 1024): + if chunk: + f.write(chunk) + + log.info(f"Done saving project archive to disk") + return file_path diff --git a/ansys/rep/client/jms/client.py b/ansys/rep/client/jms/client.py deleted file mode 100644 index 8879f67f4..000000000 --- a/ansys/rep/client/jms/client.py +++ /dev/null @@ -1,227 +0,0 @@ -# ---------------------------------------------------------- -# Copyright (C) 2019 by -# ANSYS Switzerland GmbH -# www.ansys.com -# -# Author(s): F.Negri, O.Koenig -# ---------------------------------------------------------- - -from ..auth import authenticate -from ..connection import create_session -from ..exceptions import raise_for_status -from .resource.evaluator import get_evaluators, update_evaluators -from .resource.project import ( - archive_project, - copy_project, - create_project, - delete_project, - get_project, - get_projects, - restore_project, - update_project, -) -from .resource.task_definition_template import ( - create_task_definition_templates, - delete_task_definition_templates, - get_task_definition_templates, - update_task_definition_templates, -) - - -class Client(object): - """A python interface to the Design Point Service API. - - Uses the provided credentials to create and store - an authorized :class:`requests.Session` object. - - The following authentication workflows are supported: - - - Username and password: the client connects to the OAuth server and - requests access and refresh tokens. - - Refresh token: the client connects to the OAuth server and - requests a new access token. - - Access token: no authentication needed. - - Args: - rep_url (str): The base path for the server to call, - e.g. "https://127.0.0.1/dcs". - username (str): Username (Optional) - password (str): Password (Optional) - refresh_token (str): Refresh Token (Optional) - access_token (str): Access Token (Optional) - - Example: - - >>> from ansys.rep.client.jms import Client - >>> # Create client object and connect to DCS with username & password - >>> cl = Client(rep_url="https://127.0.0.1/dcs", username="dcadmin", password="dcadmin") - >>> # Extract refresh token to eventually store it - >>> refresh_token = cl.refresh_token - >>> # Alternative: Create client object and connect to DCS with refresh token - >>> cl = Client(rep_url="https://127.0.0.1/dcs", refresh_token=refresh_token) - - """ - - def __init__( - self, - rep_url: str = "https://127.0.0.1:8443/rep", - username: str = "repadmin", - password: str = "repadmin", - *, - realm: str = "rep", - grant_type: str = "password", - scope="openid", - client_id: str = "rep-cli", - client_secret: str = None, - access_token: str = None, - refresh_token: str = None, - auth_url: str = None, - ): - - self.rep_url = rep_url - self.jms_api_url = self.rep_url + "/jms/api/v1" - self.auth_url = auth_url - self.refresh_token = None - - if access_token: - self.access_token = access_token - else: - tokens = authenticate( - url=auth_url or rep_url, - realm=realm, - grant_type=grant_type, - scope=scope, - client_id=client_id, - client_secret=client_secret, - username=username, - password=password, - refresh_token=refresh_token, - ) - self.access_token = tokens["access_token"] - self.refresh_token = tokens["refresh_token"] - - self.session = create_session(self.access_token) - # register hook to handle expiring of the refresh token - self.session.hooks["response"] = [self._auto_refresh_token, raise_for_status] - self.unauthorized_num_retry = 0 - self._unauthorized_max_retry = 1 - - def _auto_refresh_token(self, response, *args, **kwargs): - """Hook function to automatically refresh the access token and - re-send the request in case of unauthorized error""" - if ( - response.status_code == 401 - and self.unauthorized_num_retry < self._unauthorized_max_retry - ): - self.unauthorized_num_retry += 1 - self.refresh_access_token() - response.request.headers.update( - {"Authorization": self.session.headers["Authorization"]} - ) - return self.session.send(response.request) - - self.unauthorized_num_retry = 0 - return response - - def refresh_access_token(self): - """Use the refresh token to obtain a new access token""" - tokens = authenticate(url=self.auth_url or self.rep_url, refresh_token=self.refresh_token) - self.access_token = tokens["access_token"] - self.refresh_token = tokens["refresh_token"] - self.session.headers.update({"Authorization": "Bearer %s" % tokens["access_token"]}) - - def get_api_info(self): - """Return info like version, build date etc of the DPS API the client is connected to""" - r = self.session.get(self.jms_api_url) - return r.json() - - def get_projects(self, **query_params): - """Return a list of projects, optionally filtered by given query parameters""" - return get_projects(self, **query_params) - - def get_project(self, id=None, name=None): - """Return a single project for given project id""" - return get_project(self, id, name) - - def create_project(self, project, replace=False, as_objects=True): - """Create a new project""" - return create_project(self, project, replace, as_objects) - - def update_project(self, project, as_objects=True): - """Update an existing project""" - return update_project(self, project, as_objects) - - def delete_project(self, project): - """Delete a project""" - return delete_project(self, project) - - def copy_project(self, project, project_target_name): - """Copy an existing project""" - return copy_project(self, project.id, project_target_name) - - def archive_project(self, project, path, include_job_files=True): - """Archive an existing project and save it to disk - - Args: - project (:class:`ansys.rep.client.jms.Project`): A Project object - (only the id field is needed). - path (str): Where to save the archive locally. - include_job_files (bool, optional): Whether to include design point files in the - archive. True by default. - - Returns: - str: The path to the archive. - """ - return archive_project(self, project, path, include_job_files) - - def restore_project(self, path, project_name): - """Restore a project from an archive - - Args: - path (str): Path of the archive file to be restored. - project_id (str): ID of the restored project. - - Returns: - :class:`ansys.rep.client.jms.Project`: A Project object. - """ - return restore_project(self, path, project_name) - - def get_evaluators(self, as_objects=True, **query_params): - """Return a list of evaluators, optionally filtered by given query parameters""" - return get_evaluators(self, as_objects=as_objects, **query_params) - - def update_evaluators(self, evaluators, as_objects=True): - """Update evaluators job_definition""" - return update_evaluators(self, evaluators, as_objects=as_objects) - - def get_task_definition_templates(self, as_objects=True, **query_params): - """Return a list of task definition templates, - optionally filtered by given query parameters""" - return get_task_definition_templates(self, as_objects=as_objects, **query_params) - - def create_task_definition_templates(self, templates): - """Create new task definition templates - - Args: - templates (list of :class:`ansys.rep.client.jms.TaskDefinitionTemplate`): - A list of task definition templates - """ - return create_task_definition_templates(self, templates) - - def update_task_definition_templates(self, templates): - """Update existing task definition templates - - Args: - templates (list of :class:`ansys.rep.client.jms.TaskDefinitionTemplate`): - A list of task definition templates - """ - return update_task_definition_templates(self, templates) - - def delete_task_definition_templates(self, templates): - """Delete existing task definition templates - - Args: - templates (list of :class:`ansys.rep.client.jms.TaskDefinitionTemplate`): - A list of task definition templates - """ - return delete_task_definition_templates(self, templates) diff --git a/ansys/rep/client/jms/resource/__init__.py b/ansys/rep/client/jms/resource/__init__.py index 0fd87782d..96c13354c 100644 --- a/ansys/rep/client/jms/resource/__init__.py +++ b/ansys/rep/client/jms/resource/__init__.py @@ -10,8 +10,10 @@ from .evaluator import Evaluator from .file import File from .fitness_definition import FitnessDefinition, FitnessTermDefinition -from .job_definition import Job, JobDefinition +from .job import Job +from .job_definition import JobDefinition from .license_context import LicenseContext +from .operation import Operation from .parameter_definition import ( BoolParameterDefinition, FloatParameterDefinition, @@ -21,7 +23,7 @@ from .parameter_mapping import ParameterMapping from .project import Project from .project_permission import ProjectPermission -from .selection import Selection +from .selection import JobSelection from .task import Task from .task_definition import ( Licensing, diff --git a/ansys/rep/client/jms/resource/algorithm.py b/ansys/rep/client/jms/resource/algorithm.py index c551ab711..7b0f82c50 100644 --- a/ansys/rep/client/jms/resource/algorithm.py +++ b/ansys/rep/client/jms/resource/algorithm.py @@ -8,9 +8,7 @@ import logging from ..schema.algorithm import AlgorithmSchema -from .base import Object, get_objects -from .job import Job -from .selection import get_selections +from .base import Object log = logging.getLogger(__name__) @@ -19,7 +17,6 @@ class Algorithm(Object): """Algorithm resource. Args: - project (:class:`ansys.rep.client.jms.Project`, optional): Project object. Defaults to None. **kwargs: Arbitrary keyword arguments, see the Algorithm schema below. Example: @@ -38,29 +35,8 @@ class Meta: schema = AlgorithmSchema rest_name = "algorithms" - def __init__(self, project=None, **kwargs): - self.project = project + def __init__(self, **kwargs): super(Algorithm, self).__init__(**kwargs) - def get_jobs(self, as_objects=True, **query_params): - """Return a list of design points, optionally filtered by given query parameters - - Returns: - List of :class:`ansys.rep.client.jms.Job` or list of dict if as_objects is False - """ - return get_objects( - self.project, Job, as_objects=as_objects, algorithm_id=self.id, **query_params - ) - - def get_selections(self, as_objects=True, **query_params): - """Return a list of selections, optionally filtered by given query parameters - - Returns: - List of :class:`ansys.rep.client.jms.Selection` or list of dict if as_objects is False - """ - return get_selections( - self.project, as_objects=as_objects, algorithm_id=self.id, **query_params - ) - AlgorithmSchema.Meta.object_class = Algorithm diff --git a/ansys/rep/client/jms/resource/base.py b/ansys/rep/client/jms/resource/base.py index 06476aa58..5c3474d19 100644 --- a/ansys/rep/client/jms/resource/base.py +++ b/ansys/rep/client/jms/resource/base.py @@ -10,8 +10,6 @@ from marshmallow.utils import missing -from ansys.rep.client import REPError - log = logging.getLogger(__name__) @@ -55,116 +53,6 @@ def __repr__(self): self.__class__.__name__, ",".join(["%s=%r" % (k, getattr(self, k)) for k in self.declared_fields()]), ) - # return "%s(%s)" % (self.__class__.__name__, - # ",".join(["%s=%r" %(k,v) for k,v in self.__dict__.items()]) ) def __str__(self): - return "%s(\n%s\n)" % ( - self.__class__.__name__, - ",\n".join(["%s=%r" % (k, getattr(self, k)) for k in self.declared_fields()]), - ) - # return "{%s\n}" % ("\n".join(["%s: %s" %(k,str(v)) for k,v in self.__dict__.items()]) ) - - -def get_objects(project, obj_type, job_definition=None, as_objects=True, **query_params): - """Reusable function to get objects in a project""" - rest_name = obj_type.Meta.rest_name - url = f"{project.client.jms_api_url}/projects/{project.id}" - if job_definition is not None: - url += f"/job_definitions/{job_definition.id}" - url += f"/{rest_name}" - - query_params.setdefault("fields", "all") - - r = project.client.session.get(url, params=query_params) - - if query_params.get("count"): - return r.json()[f"num_{rest_name}"] - - data = r.json()[rest_name] - if not as_objects: - return data - - schema = obj_type.Meta.schema(many=True) - objects = schema.load(data) - for o in objects: - o.project = project - return objects - - -def create_objects(project, objects, as_objects=True, **query_params): - - if not objects: - return [] - - are_same = [o.__class__ == objects[0].__class__ for o in objects[1:]] - if not all(are_same): - raise REPError("Mixed object types") - - obj_type = objects[0].__class__ - rest_name = obj_type.Meta.rest_name - url = f"{project.client.jms_api_url}/projects/{project.id}/{rest_name}" - - query_params.setdefault("fields", "all") - - schema = obj_type.Meta.schema(many=True) - serialized_data = schema.dump(objects) - json_data = json.dumps({rest_name: serialized_data}) - r = project.client.session.post(f"{url}", data=json_data, params=query_params) - - data = r.json()[rest_name] - if not as_objects: - return data - - objects = schema.load(data) - for o in objects: - o.project = project - return objects - - -def update_objects(project, objects, as_objects=True, **query_params): - - if not objects: - return [] - - are_same = [o.__class__ == objects[0].__class__ for o in objects[1:]] - if not all(are_same): - raise REPError("Mixed object types") - - obj_type = objects[0].__class__ - rest_name = obj_type.Meta.rest_name - url = f"{project.client.jms_api_url}/projects/{project.id}/{rest_name}" - - query_params.setdefault("fields", "all") - - schema = obj_type.Meta.schema(many=True) - serialized_data = schema.dump(objects) - json_data = json.dumps({rest_name: serialized_data}) - r = project.client.session.put(f"{url}", data=json_data, params=query_params) - - data = r.json()[rest_name] - if not as_objects: - return data - - objects = schema.load(data) - for o in objects: - o.project = project - return objects - - -def delete_objects(project, objects): - """Reusable delete function""" - - if not objects: - return - - are_same = [o.__class__ == objects[0].__class__ for o in objects[1:]] - if not all(are_same): - raise REPError("Mixed object types") - - obj_type = objects[0].__class__ - rest_name = obj_type.Meta.rest_name - url = f"{project.client.jms_api_url}/projects/{project.id}/{rest_name}" - - data = json.dumps({"source_ids": [obj.id for obj in objects]}) - r = project.client.session.delete(url, data=data) + return json.dumps(self.Meta.schema(many=False).dump(self), indent=2) diff --git a/ansys/rep/client/jms/resource/evaluator.py b/ansys/rep/client/jms/resource/evaluator.py index b1e5aa0f8..ff33909e2 100644 --- a/ansys/rep/client/jms/resource/evaluator.py +++ b/ansys/rep/client/jms/resource/evaluator.py @@ -5,8 +5,6 @@ # # Author(s): F.Negri # ---------------------------------------------------------- -import json - from ..schema.evaluator import EvaluatorSchema from .base import Object @@ -30,45 +28,5 @@ class Meta: def __init__(self, **kwargs): super(Evaluator, self).__init__(**kwargs) - # def __init__(self, project=None, **kwargs): - # self.project=project - # super(Evaluator, self).__init__(**kwargs) - EvaluatorSchema.Meta.object_class = Evaluator - - -def get_evaluators(client, as_objects=True, **query_params): - """ - Returns list of evaluators - """ - url = f"{client.jms_api_url}/evaluators" - query_params.setdefault("fields", "all") - r = client.session.get(url, params=query_params) - - data = r.json()["evaluators"] - if not as_objects: - return data - - evaluators = EvaluatorSchema(many=True).load(data) - return evaluators - - -def update_evaluators(client, evaluators, as_objects=True, **query_params): - """ - Update evaluator job_definition - """ - url = f"{client.jms_api_url}/evaluators" - - schema = EvaluatorSchema(many=True) - serialized_data = schema.dump(evaluators) - json_data = json.dumps({"evaluators": [serialized_data]}) - query_params.setdefault("fields", "all") - r = client.session.put(f"{url}", data=json_data, params=query_params) - - data = r.json()["evaluators"][0] - if not as_objects: - return data - - objects = schema.load(data) - return objects diff --git a/ansys/rep/client/jms/resource/file.py b/ansys/rep/client/jms/resource/file.py index a61692250..5edad4409 100644 --- a/ansys/rep/client/jms/resource/file.py +++ b/ansys/rep/client/jms/resource/file.py @@ -6,13 +6,9 @@ # Author(s): O.Koenig # ---------------------------------------------------------- import logging -import os -from pathlib import Path - -from ansys.rep.client.exceptions import REPError from ..schema.file import FileSchema -from .base import Object, create_objects, get_objects, update_objects +from .base import Object log = logging.getLogger(__name__) @@ -21,8 +17,6 @@ class File(Object): """File resource. Args: - project (:class:`ansys.rep.client.jms.Project`, optional): Project resource. - Defaults to None. src (str, optional): Path to the local file. Defaults to None. **kwargs: Arbitrary keyword arguments, see the File schema below. @@ -44,108 +38,10 @@ class Meta: schema = FileSchema rest_name = "files" - def __init__(self, project=None, src=None, **kwargs): - self.project = project + def __init__(self, src=None, **kwargs): self.src = src self.content = None super(File, self).__init__(**kwargs) - def download(self, target_path, stream=True, progress_handler=None): - """ - Download file content and save it to disk. If stream=True, - data is retrieved in chunks, avoiding storing the entire content - in memory. - """ - return download_file(self, target_path, progress_handler, stream) - FileSchema.Meta.object_class = File - - -def _download_files(files): - """ - Temporary implementation of file download directly using fs REST gateway. - To be replaced with direct ansft calls, when it is available as python pkg - """ - - for f in files: - if getattr(f, "hash", None) is not None: - r = f.project.client.session.get(f"{f.project.fs_bucket_url}/{f.storage_id}") - f.content = r.content - f.content_type = r.headers["Content-Type"] - - -def get_files(project, as_objects=True, content=False, **query_params): - - files = get_objects(project, File, as_objects=as_objects, **query_params) - for file in files: - file.project = project - if content: - _download_files(files) - return files - - -def _upload_files(project, files): - """ - Temporary implementation of file upload directly using fs REST gateway. - To be replaced with direct ansft calls, when it is available as python pkg - """ - fs_headers = {"content-type": "application/octet-stream"} - - for f in files: - if getattr(f, "src", None) is not None: - with open(f.src, "rb") as file_content: - r = project.client.session.post( - f"{project.fs_bucket_url}/{f.storage_id}", data=file_content, headers=fs_headers - ) - f.hash = r.json()["checksum"] - f.size = os.path.getsize(f.src) - - -def create_files(project, files, as_objects=True): - # (1) Create file resources in DPS - created_files = create_objects(project, files, as_objects=as_objects) - - # (2) Check if there are src properties, files to upload - num_uploads = 0 - for f, cf in zip(files, created_files): - if getattr(f, "src", None) is not None: - cf.src = f.src - num_uploads += 1 - - if num_uploads > 0: - - # (3) Upload file contents - _upload_files(project, created_files) - - # (4) Update corresponding file resources in DPS with hashes of uploaded files - created_files = update_objects(project, created_files, as_objects=as_objects) - - return created_files - - -def update_files(project, files, as_objects=True): - # Upload files first if there are any src parameters - _upload_files(project, files) - # Update file resources in DPS - return update_objects(project, files, as_objects=as_objects) - - -def download_file(file, target_path, progress_handler=None, stream=True): - - if getattr(file, "hash", None) is None: - raise REPError(f"No hash found. Failed to download file {file.name}") - - Path(target_path).mkdir(parents=True, exist_ok=True) - download_link = f"{file.project.fs_bucket_url}/{file.storage_id}" - download_path = os.path.join(target_path, file.evaluation_path) - - with file.project.client.session.get(download_link, stream=stream) as r, open( - download_path, "wb" - ) as f: - for chunk in r.iter_content(chunk_size=None): - f.write(chunk) - if progress_handler is not None: - progress_handler(len(chunk)) - - return download_path diff --git a/ansys/rep/client/jms/resource/job.py b/ansys/rep/client/jms/resource/job.py index bf20657cf..312d2bd2e 100644 --- a/ansys/rep/client/jms/resource/job.py +++ b/ansys/rep/client/jms/resource/job.py @@ -9,8 +9,7 @@ import logging from ..schema.job import JobSchema -from .base import Object, get_objects, update_objects -from .task import Task +from .base import Object log = logging.getLogger(__name__) @@ -19,7 +18,6 @@ class Job(Object): """Job resource. Args: - project (:class:`ansys.rep.client.jms.Project`, optional): Project object. Defaults to None. **kwargs: Arbitrary keyword arguments, see the Job schema below. Example: @@ -39,74 +37,32 @@ class Meta: schema = JobSchema rest_name = "jobs" - def __init__(self, project=None, **kwargs): - self.project = project + def __init__(self, **kwargs): super(Job, self).__init__(**kwargs) - def get_tasks(self, as_objects=True, **query_params): - """Return a list of tasks, optionally filtered by given query parameters - - Args: - as_objects (bool, optional): Defaults to True. - **query_params: Optional query parameters. - - Returns: - List of :class:`ansys.rep.client.jms.Task` or list of dict if as_objects is False - """ - return get_objects( - self.project, Task, job_id=self.id, as_objects=as_objects, **query_params - ) - - def update_tasks(self, tasks, as_objects=True, **query_params): - """Update existing tasks - - Args: - tasks (list of :class:`ansys.rep.client.jms.Task`): A list of task objects - as_objects (bool): Whether to return tasks as objects or dictionaries - - Returns: - List of :class:`ansys.rep.client.jms.Task` or list of dict if `as_objects` is True - """ - return update_objects( - self.project, tasks, job_id=self.id, as_objects=as_objects, **query_params - ) - - def _sync(self): - sync_jobs(project=self.project, jobs=[self]) - JobSchema.Meta.object_class = Job -def copy_jobs(project, jobs, job_definition=None, as_objects=True, **query_params): - """Create new design points by copying existing ones""" +def copy_jobs(project_api, jobs, as_objects=True, **query_params): + """Create new jobs by copying existing ones""" - url = f"{project.client.jms_api_url}/projects/{project.id}" - if job_definition: - url += f"/job_definitions/{job_definition.id}" - url += f"/jobs" + url = f"{project_api.url}/jobs" query_params.setdefault("fields", "all") json_data = json.dumps({"source_ids": [obj.id for obj in jobs]}) - r = project.client.session.post(f"{url}", data=json_data, params=query_params) + r = project_api.client.session.post(f"{url}", data=json_data, params=query_params) data = r.json()["jobs"] if not as_objects: return data - objects = JobSchema(many=True).load(data) - for o in objects: - o.project = project - return objects - + return JobSchema(many=True).load(data) -def sync_jobs(project, jobs, job_definition=None): - url = f"{project.client.jms_api_url}/projects/{project.id}" - if job_definition: - url += f"/job_definitions/{job_definition.id}" - url += f"/jobs:sync" +def sync_jobs(project_api, jobs): + url = f"{project_api.url}/jobs:sync" json_data = json.dumps({"job_ids": [obj.id for obj in jobs]}) - r = project.client.session.put(f"{url}", data=json_data) + r = project_api.client.session.put(f"{url}", data=json_data) diff --git a/ansys/rep/client/jms/resource/job_definition.py b/ansys/rep/client/jms/resource/job_definition.py index 690c250f4..eb71d4f3e 100644 --- a/ansys/rep/client/jms/resource/job_definition.py +++ b/ansys/rep/client/jms/resource/job_definition.py @@ -8,8 +8,7 @@ import logging from ..schema.job_definition import JobDefinitionSchema -from .base import Object, create_objects, delete_objects, get_objects -from .job import Job, copy_jobs +from .base import Object log = logging.getLogger(__name__) @@ -18,8 +17,6 @@ class JobDefinition(Object): """JobDefinition resource. Args: - project (:class:`ansys.rep.client.jms.Project`, optional): A Project object. - Defaults to None. **kwargs: Arbitrary keyword arguments, see the JobDefinition schema below. Example: @@ -40,24 +37,8 @@ class Meta: schema = JobDefinitionSchema rest_name = "job_definitions" - def __init__(self, project=None, **kwargs): - self.project = project + def __init__(self, **kwargs): super(JobDefinition, self).__init__(**kwargs) - def get_jobs(self, **query_params): - """Return a list of design points, optionally filtered by given query parameters""" - return get_objects(self.project, Job, job_definition=self, **query_params) - - def create_jobs(self, jobs): - for j in jobs: - j.job_definition_id = self.id - return create_objects(self.project, jobs) - - def copy_jobs(self, jobs): - return copy_jobs(self.project, jobs, job_definition=self) - - def delete_jobs(self, jobs): - return delete_objects(self.project, jobs) - JobDefinitionSchema.Meta.object_class = JobDefinition diff --git a/ansys/rep/client/jms/resource/operation.py b/ansys/rep/client/jms/resource/operation.py new file mode 100644 index 000000000..b60b341df --- /dev/null +++ b/ansys/rep/client/jms/resource/operation.py @@ -0,0 +1,26 @@ +import logging + +from ..schema.operation import OperationSchema +from .base import Object + +log = logging.getLogger(__name__) + + +class Operation(Object): + """Operation resource. + + The operation schema has the following fields: + + .. jsonschema:: schemas/Operation.json + + """ + + class Meta: + schema = OperationSchema + rest_name = "operations" + + def __init__(self, **kwargs): + super(Operation, self).__init__(**kwargs) + + +OperationSchema.Meta.object_class = Operation diff --git a/ansys/rep/client/jms/resource/project.py b/ansys/rep/client/jms/resource/project.py index 727942060..acc613066 100644 --- a/ansys/rep/client/jms/resource/project.py +++ b/ansys/rep/client/jms/resource/project.py @@ -5,11 +5,7 @@ # # Author(s): O.Koenig # ---------------------------------------------------------- -import json import logging -import os -import time -import uuid from cachetools import TTLCache, cached from marshmallow.utils import missing @@ -18,18 +14,7 @@ from ansys.rep.client.exceptions import REPError from ..schema.project import ProjectSchema -from .algorithm import Algorithm -from .base import Object, create_objects, delete_objects, get_objects, update_objects -from .file import create_files, get_files, update_files -from .job import Job, copy_jobs, sync_jobs -from .job_definition import JobDefinition -from .license_context import LicenseContext -from .parameter_definition import ParameterDefinition -from .parameter_mapping import ParameterMapping -from .project_permission import ProjectPermission, update_permissions -from .selection import create_selections, delete_selections, get_selections, update_selections -from .task import Task -from .task_definition import TaskDefinition +from .base import Object log = logging.getLogger(__name__) @@ -38,7 +23,6 @@ class Project(Object): """Project resource Args: - client (:class:`ansys.rep.client.jms.Client`, optional): A client object. Defaults to None. **kwargs: Arbitrary keyword arguments, see the Project schema below. Example: @@ -55,443 +39,17 @@ class Project(Object): class Meta: schema = ProjectSchema - def __init__(self, client=None, **kwargs): - self.client = client + def __init__(self, **kwargs): super(Project, self).__init__(**kwargs) - ################################################################ - # Files - def get_files(self, as_objects=True, content=False, **query_params): - """ - Return a list of file resources, optionally filtered by given query parameters. - If content=True, each files content is downloaded as well and stored in memory - as :attr:`ansys.rep.client.jms.File.content`. - """ - return get_files(self, as_objects=as_objects, content=content, **query_params) - - def create_files(self, files, as_objects=True): - return create_files(self, files, as_objects=as_objects) - - def update_files(self, files, as_objects=True): - return update_files(self, files, as_objects=as_objects) - - def delete_files(self, files): - return delete_objects(self, files) - - ################################################################ - # Parameter definitions - def get_parameter_definitions(self, as_objects=True, **query_params): - return get_objects(self, ParameterDefinition, as_objects=as_objects, **query_params) - - def create_parameter_definitions(self, parameter_definitions, as_objects=True): - return create_objects(self, parameter_definitions, as_objects=as_objects) - - def update_parameter_definitions(self, parameter_definitions, as_objects=True): - return update_objects(self, parameter_definitions, as_objects=as_objects) - - def delete_parameter_definitions(self, parameter_definitions): - return delete_objects(self, parameter_definitions) - - ################################################################ - # Parameter mappings - def get_parameter_mappings(self, as_objects=True, **query_params): - return get_objects(self, ParameterMapping, as_objects=as_objects, **query_params) - - def create_parameter_mappings(self, parameter_mappings, as_objects=True): - return create_objects(self, parameter_mappings, as_objects=as_objects) - - def update_parameter_mappings(self, parameter_mappings, as_objects=True): - return update_objects(self, parameter_mappings, as_objects=as_objects) - - def delete_parameter_mappings(self, parameter_mappings): - return delete_objects(self, parameter_mappings) - - ################################################################ - # Task definitions - def get_task_definitions(self, as_objects=True, **query_params): - return get_objects(self, TaskDefinition, as_objects=as_objects, **query_params) - - def create_task_definitions(self, task_definitions, as_objects=True): - return create_objects(self, task_definitions, as_objects=as_objects) - - def update_task_definitions(self, task_definitions, as_objects=True): - return update_objects(self, task_definitions, as_objects=as_objects) - - def delete_task_definitions(self, task_definitions): - return delete_objects(self, task_definitions) - - ################################################################ - # Job definitions - def get_job_definitions(self, as_objects=True, **query_params): - return get_objects(self, JobDefinition, as_objects=as_objects, **query_params) - - def create_job_definitions(self, job_definitions, as_objects=True): - return create_objects(self, job_definitions, as_objects=as_objects) - - def update_job_definitions(self, job_definitions, as_objects=True): - return update_objects(self, job_definitions, as_objects=as_objects) - - def delete_job_definitions(self, job_definitions): - return delete_objects(self, job_definitions) - - ################################################################ - # Jobs - def get_jobs(self, as_objects=True, **query_params): - return get_objects(self, Job, as_objects=as_objects, **query_params) - - def create_jobs(self, jobs, as_objects=True): - """Create new design points - - Args: - jobs (list of :class:`ansys.rep.client.jms.Job`): A list of design point objects - as_objects (bool): Whether to return design points as objects or dictionaries - - Returns: - List of :class:`ansys.rep.client.jms.Job` or list of dict if `as_objects` is False - """ - return create_objects(self, jobs, as_objects=as_objects) - - def copy_jobs(self, jobs, as_objects=True): - """Create new design points by copying existing ones - - Args: - jobs (list of :class:`ansys.rep.client.jms.Job`): A list of design point objects - - Note that only the ``id`` field of the design point objects need to be filled; - the other fields can be empty. - """ - return copy_jobs(self, jobs, as_objects=as_objects) - - def update_jobs(self, jobs, as_objects=True): - """Update existing design points - - Args: - jobs (list of :class:`ansys.rep.client.jms.Job`): A list of design point objects - as_objects (bool): Whether to return design points as objects or dictionaries - - Returns: - List of :class:`ansys.rep.client.jms.Job` or list of dict if `as_objects` is True - """ - return update_objects(self, jobs, as_objects=as_objects) - - def delete_jobs(self, jobs): - """Delete existing design points - - Args: - jobs (list of :class:`ansys.rep.client.jms.Job`): A list of design point objects - - Note that only the ``id`` field of the design point objects need to be filled; - the other fields can be empty. - - Example: - - >>> dps_to_delete = [] - >>> for id in [1,2,39,44]: - >>> dps_to_delete.append(Job(id=id)) - >>> project.delete_jobs(dps_to_delete) - - """ - return delete_objects(self, jobs) - - def _sync_jobs(self, jobs): - return sync_jobs(self, jobs) - - ################################################################ - # Tasks - def get_tasks(self, as_objects=True, **query_params): - return get_objects(self, Task, as_objects=as_objects, **query_params) - - def update_tasks(self, tasks, as_objects=True): - return update_objects(self, tasks, as_objects=as_objects) - - ################################################################ - # Selections - def get_selections(self, as_objects=True, **query_params): - return get_selections(self, as_objects=as_objects, **query_params) - - def create_selections(self, selections, as_objects=True): - return create_selections(self, selections, as_objects=as_objects) - - def update_selections(self, selections, as_objects=True): - return update_selections(self, selections, as_objects=as_objects) - - def delete_selections(self, selections): - return delete_selections(self, selections) - - ################################################################ - # Algorithms - def get_algorithms(self, as_objects=True, **query_params): - return get_objects(self, Algorithm, as_objects=as_objects, **query_params) - - def create_algorithms(self, algorithms, as_objects=True): - return create_objects(self, algorithms, as_objects=as_objects) - - def update_algorithms(self, algorithms, as_objects=True): - return update_objects(self, algorithms, as_objects=as_objects) - - def delete_algorithms(self, algorithms): - return delete_objects(self, algorithms) - - ################################################################ - # Permissions - def get_permissions(self, as_objects=True): - return get_objects(self, ProjectPermission, as_objects=as_objects) - - def update_permissions(self, permissions): - # the rest api currently doesn't return anything on permissions update - update_permissions(self, permissions) - - ################################################################ - # License contexts - def get_license_contexts(self, as_objects=True, **query_params): - return get_objects(self, LicenseContext, as_objects=as_objects, **query_params) - - def create_license_contexts(self, as_objects=True): - rest_name = LicenseContext.Meta.rest_name - url = f"{self.client.jms_api_url}/projects/{self.id}/{rest_name}" - r = self.client.session.post(f"{url}") - data = r.json()[rest_name] - if not as_objects: - return data - schema = LicenseContext.Meta.schema(many=True) - objects = schema.load(data) - return objects - - def update_license_contexts(self, license_contexts, as_objects=True): - return update_objects(self, license_contexts, as_objects=as_objects) - - def delete_license_contexts(self): - rest_name = LicenseContext.Meta.rest_name - url = f"{self.client.jms_api_url}/projects/{self.id}/{rest_name}" - r = self.client.session.delete(url) - - ################################################################ - # Others - def copy(self, project_name): - """Create a copy of the project - - Args: - project_id (str): ID of the new project. - """ - return copy_project(self.client, self.id, project_name) - - @property - def fs_url(self): - """URL of the file storage gateway""" - return get_fs_url(self) - - @property - def fs_bucket_url(self): - """URL of the project's bucket in the file storage gateway""" - return f"{get_fs_url(self)}/{self.id}" - ProjectSchema.Meta.object_class = Project -def get_projects(client, as_objects=True, **query_params): - """ - Returns list of projects - """ - url = f"{client.jms_api_url}/projects" - r = client.session.get(url, params=query_params) - - data = r.json()["projects"] - if not as_objects: - return data - - schema = ProjectSchema(many=True) - projects = schema.load(data) - for p in projects: - p.client = client - return projects - - -def get_project(client, id=None, name=None): - """ - Return a single project - """ - params = {} - if name: - url = f"{client.jms_api_url}/projects/" - params["name"] = name - else: - url = f"{client.jms_api_url}/projects/{id}" - - r = client.session.get(url, params=params) - - if len(r.json()["projects"]): - schema = ProjectSchema() - project = schema.load(r.json()["projects"][0]) - project.client = client - return project - return None - - -def create_project(client, project, replace=False, as_objects=True): - url = f"{client.jms_api_url}/projects/" - - schema = ProjectSchema() - serialized_data = schema.dump(project) - json_data = json.dumps({"projects": [serialized_data], "replace": replace}) - r = client.session.post(f"{url}", data=json_data) - - data = r.json()["projects"][0] - if not as_objects: - return data - - project = schema.load(data) - project.client = client - return project - - -def update_project(client, project, as_objects=True): - url = f"{client.jms_api_url}/projects/{project.id}" - - schema = ProjectSchema() - serialized_data = schema.dump(project) - json_data = json.dumps({"projects": [serialized_data]}) - r = client.session.put(f"{url}", data=json_data) - - data = r.json()["projects"][0] - if not as_objects: - return data - - project = schema.load(data) - project.client = client - return project - - -def delete_project(client, project): - - url = f"{client.jms_api_url}/projects/{project.id}" - r = client.session.delete(url) - - -def copy_project(client, project_source_id, project_target_name, wait=True): - - url = f"{client.jms_api_url}/projects/{project_source_id}/copy" - r = client.session.put(url, params={"project_name": project_target_name}) - - operation_location = r.headers["location"] - - if not wait: - return operation_location - - op = _monitor_operation(client, operation_location, 1.0) - if not op["succeeded"]: - raise REPError(f"Failed to copy project {project_source_id}.") - return op["result"] - - -def archive_project(client, project, target_path, include_job_files=True): - - # PUT archive request - url = f"{client.jms_api_url}/projects/{project.id}/archive" - query_params = {} - if not include_job_files: - query_params["download_files"] = "job_definition" - - r = client.session.put(url, params=query_params) - - # Monitor archive operation - operation_location = r.headers["location"] - log.debug(f"Operation location: {operation_location}") - - op = _monitor_operation(client, operation_location, 1.0) - - if not op["succeeded"]: - raise REPError(f"Failed to archive project {project.id}.") - - download_link = op["result"]["backend_path"] - - # Download archive - download_link = download_link.replace("ansfs://", project.fs_url + "/") - log.info(f"Project archive download link: {download_link}") - - if not os.path.isdir(target_path): - raise REPError(f"Project archive: target path does not exist {target_path}") - - file_path = os.path.join(target_path, download_link.rsplit("/")[-1]) - log.info(f"Download archive to {file_path}") - - with client.session.get(download_link, stream=True) as r: - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=1024 * 1024): - if chunk: - f.write(chunk) - - log.info(f"Done saving project archive to disk") - return file_path - - -def restore_project(client, archive_path, project_name): - - if not os.path.exists(archive_path): - raise REPError(f"Project archive: path does not exist {archive_path}") - - # Upload archive to FS API - archive_name = os.path.basename(archive_path) - - bucket = f"rep-client-restore-{uuid.uuid4()}" - fs_file_url = f"{client.rep_url}/fs/api/v1/{bucket}/{archive_name}" - ansfs_file_url = f"ansfs://{bucket}/{archive_name}" - - fs_headers = {"content-type": "application/octet-stream"} - - log.info(f"Uploading archive to {fs_file_url}") - with open(archive_path, "rb") as file_content: - r = client.session.post(fs_file_url, data=file_content, headers=fs_headers) - - # POST restore request - log.info(f"Restoring archive from {ansfs_file_url}") - url = f"{client.jms_api_url}/projects/archive" - query_params = {"backend_path": ansfs_file_url} - r = client.session.post(url, params=query_params) - - # Monitor restore operation - operation_location = r.headers["location"] - log.debug(f"Operation location: {operation_location}") - - op = _monitor_operation(client, operation_location, 1.0) - - if not op["succeeded"]: - raise REPError(f"Failed to restore project from archive {archive_path}.") - - project_id = op["result"] - log.info("Done restoring project") - - # Delete archive file on server - log.info(f"Delete file bucket {fs_file_url}") - r = client.session.put(f"{client.rep_url}/fs/api/v1/remove/{bucket}") - - return get_project(client, project_id) - - -def _monitor_operation(client, location, interval=1.0): - - done = False - op = None - - while not done: - r = client.session.get(f"{client.jms_api_url}{location}") - if len(r.json()["operations"]) > 0: - op = r.json()["operations"][0] - done = op["finished"] - log.info( - f"""Operation {op['name']} - progress={op['progress'] * 100.0}%, - succeeded={op['succeeded']}, finished={op['finished']}""" - ) - time.sleep(interval) - - return op - - @cached(cache=TTLCache(1024, 60), key=lambda project: project.id) -def get_fs_url(project): +def get_fs_url(project: Project): if project.file_storages == missing: raise REPError(f"The project object has no file storages information.") - rest_gateways = [fs for fs in project.file_storages if fs["obj_type"] == "RestGateway"] rest_gateways.sort(key=lambda fs: fs["priority"], reverse=True) diff --git a/ansys/rep/client/jms/resource/project_permission.py b/ansys/rep/client/jms/resource/project_permission.py index bc3672e9c..dee6da352 100644 --- a/ansys/rep/client/jms/resource/project_permission.py +++ b/ansys/rep/client/jms/resource/project_permission.py @@ -19,22 +19,21 @@ class Meta: schema = ProjectPermissionSchema rest_name = "permissions" - def __init__(self, project=None, **kwargs): - self.project = project + def __init__(self, **kwargs): super(ProjectPermission, self).__init__(**kwargs) ProjectPermissionSchema.Meta.object_class = ProjectPermission -def update_permissions(project, permissions): +def update_permissions(client, project_api_url, permissions): if not permissions: return - url = f"{project.client.jms_api_url}/projects/{project.id}/permissions" + url = f"{project_api_url}/permissions" schema = ProjectPermissionSchema(many=True) serialized_data = schema.dump(permissions) json_data = json.dumps({"permissions": serialized_data}) - r = project.client.session.put(f"{url}", data=json_data) + r = client.session.put(f"{url}", data=json_data) diff --git a/ansys/rep/client/jms/resource/selection.py b/ansys/rep/client/jms/resource/selection.py index bfa2a70c6..06fcf25e1 100644 --- a/ansys/rep/client/jms/resource/selection.py +++ b/ansys/rep/client/jms/resource/selection.py @@ -5,117 +5,36 @@ # # Author(s): O.Koenig # ---------------------------------------------------------- -import json import logging -from ..schema.selection import SelectionSchema -from .base import Object, get_objects -from .job_definition import Job +from ..schema.selection import JobSelectionSchema +from .base import Object log = logging.getLogger(__name__) -class Selection(Object): - """Selection resource. +class JobSelection(Object): + """JobSelection resource. Args: - project (:class:`ansys.rep.client.jms.Project`, optional): Project object. Defaults to None. **kwargs: Arbitrary keyword arguments, see the Selection schema below. Example: - >>> sel = Selection(name="selection_0", jobs=[1,2,15,28,45]) + >>> sel = JobSelection(name="selection_0", jobs=[1,2,15,28,45]) - The Selection schema has the following fields: + The JobSelection schema has the following fields: - .. jsonschema:: schemas/Selection.json + .. jsonschema:: schemas/JobSelection.json """ class Meta: - schema = SelectionSchema + schema = JobSelectionSchema + rest_name = "job_selections" - def __init__(self, project=None, **kwargs): - self.project = project - super(Selection, self).__init__(**kwargs) + def __init__(self, **kwargs): + super(JobSelection, self).__init__(**kwargs) - def get_jobs(self, as_objects=True, **query_params): - """Return a list of design points, optionally filtered by given query parameters - Returns: - List of :class:`ansys.rep.client.jms.Job` or list of dict if as_objects is True - """ - return get_objects( - self.project, Job, as_objects=as_objects, selection=self.name, **query_params - ) - - -SelectionSchema.Meta.object_class = Selection - - -def get_selections(project, job_definition=None, as_objects=True, **query_params): - url = f"{project.client.jms_api_url}/projects/{project.id}" - if job_definition: - url += f"/job_definitions/{job_definition.id}" - url += f"/jobs/selections" - - query_params.setdefault("fields", "all") - r = project.client.session.get(url, params=query_params) - - if query_params.get("count"): - return r.json()["num_selections"] - - data = r.json()["selections"] - if not as_objects: - return data - - schema = SelectionSchema(many=True) - objects = schema.load(data) - for o in objects: - o.project = project - return objects - - -def create_selections(project, objects, as_objects=True, **query_params): - url = f"{project.client.jms_api_url}/projects/{project.id}/jobs/selections" - - schema = SelectionSchema(many=True) - serialized_data = schema.dump(objects) - json_data = json.dumps({"selections": serialized_data}) - query_params.setdefault("fields", "all") - r = project.client.session.post(f"{url}", data=json_data, params=query_params) - - data = r.json()["selections"] - if not as_objects: - return data - - objects = schema.load(data) - for o in objects: - o.project = project - return objects - - -def update_selections(project, objects, as_objects=True, **query_params): - url = f"{project.client.jms_api_url}/projects/{project.id}/jobs/selections" - - schema = SelectionSchema(many=True) - serialized_data = schema.dump(objects) - json_data = json.dumps({"selections": serialized_data}) - query_params.setdefault("fields", "all") - r = project.client.session.put(f"{url}", data=json_data, params=query_params) - - data = r.json()["selections"] - if not as_objects: - return data - - objects = schema.load(data) - for o in objects: - o.project = project - return objects - - -def delete_selections(project, objects): - url = f"{project.client.jms_api_url}/projects/{project.id}/jobs/selections" - - data = json.dumps({"source_ids": [obj.id for obj in objects]}) - r = project.client.session.delete(url, data=data) +JobSelectionSchema.Meta.object_class = JobSelection diff --git a/ansys/rep/client/jms/resource/task.py b/ansys/rep/client/jms/resource/task.py index 78041e52a..b3bce0974 100644 --- a/ansys/rep/client/jms/resource/task.py +++ b/ansys/rep/client/jms/resource/task.py @@ -17,7 +17,6 @@ class Task(Object): """Task resource. Args: - project (:class:`ansys.rep.client.jms.Project`, optional): Project object. Defaults to None. **kwargs: Arbitrary keyword arguments, see the Task schema below. The Task schema has the following fields: @@ -30,8 +29,7 @@ class Meta: schema = TaskSchema rest_name = "tasks" - def __init__(self, project=None, **kwargs): - self.project = project + def __init__(self, **kwargs): super(Task, self).__init__(**kwargs) diff --git a/ansys/rep/client/jms/resource/task_definition_template.py b/ansys/rep/client/jms/resource/task_definition_template.py index 0f2c44a1b..76d5b925e 100644 --- a/ansys/rep/client/jms/resource/task_definition_template.py +++ b/ansys/rep/client/jms/resource/task_definition_template.py @@ -6,8 +6,6 @@ # Author(s): F.Negri # ---------------------------------------------------------- -import json - from ..schema.task_definition_template import TaskDefinitionTemplateSchema from .base import Object @@ -49,73 +47,10 @@ class TaskDefinitionTemplate(Object): class Meta: schema = TaskDefinitionTemplateSchema + rest_name = "task_definition_templates" def __init__(self, **kwargs): super(TaskDefinitionTemplate, self).__init__(**kwargs) TaskDefinitionTemplateSchema.Meta.object_class = TaskDefinitionTemplate - - -def get_task_definition_templates(client, as_objects=True, **query_params): - """ - Returns list of task definition templates - """ - - url = f"{client.jms_api_url}/task_definition_templates" - r = client.session.get(url, params=query_params) - - data = r.json()["task_definition_templates"] - if not as_objects: - return data - - templates = TaskDefinitionTemplateSchema(many=True).load(data) - return templates - - -def update_task_definition_templates(client, templates): - """ - Update task definition templates - """ - url = f"{client.jms_api_url}/task_definition_templates" - - schema = TaskDefinitionTemplateSchema(many=True) - serialized_data = schema.dump(templates) - json_data = json.dumps({"task_definition_templates": serialized_data}) - - r = client.session.put(f"{url}", data=json_data) - - data = r.json()["task_definition_templates"] - - objects = schema.load(data) - return objects - - -def create_task_definition_templates(client, templates): - """ - Create task definition templates - """ - url = f"{client.jms_api_url}/task_definition_templates" - - schema = TaskDefinitionTemplateSchema(many=True) - serialized_data = schema.dump(templates) - json_data = json.dumps({"task_definition_templates": serialized_data}) - - print(json.dumps({"task_definition_templates": serialized_data}, indent=4)) - - r = client.session.post(f"{url}", data=json_data) - - data = r.json()["task_definition_templates"] - - objects = schema.load(data) - return objects - - -def delete_task_definition_templates(client, templates): - """ - Delete task definition templates - """ - url = f"{client.jms_api_url}/task_definition_templates" - - json_data = json.dumps({"source_ids": [obj.id for obj in templates]}) - r = client.session.delete(f"{url}", data=json_data) diff --git a/ansys/rep/client/jms/schema/algorithm.py b/ansys/rep/client/jms/schema/algorithm.py index 0f6dd37df..e88d5eb95 100644 --- a/ansys/rep/client/jms/schema/algorithm.py +++ b/ansys/rep/client/jms/schema/algorithm.py @@ -30,7 +30,7 @@ class Meta(ObjectSchema.Meta): data = fields.String( allow_none=True, - description="""Generic string field to hold arbitrary algorithm job_definition data, - e.g. as JSON dict​ionary.""", + description="Generic string field to hold arbitrary algorithm job_definition data," + " e.g. as JSON dict​ionary.", ) job_ids = IdReferenceList("Job", attribute="jobs", description="List of design point IDs.") diff --git a/ansys/rep/client/jms/schema/base.py b/ansys/rep/client/jms/schema/base.py index 4c38cd9f1..e25876128 100644 --- a/ansys/rep/client/jms/schema/base.py +++ b/ansys/rep/client/jms/schema/base.py @@ -25,6 +25,6 @@ class ObjectSchema(BaseSchema): id = fields.String( allow_none=True, attribute="id", - description="""Unique ID to access the resource, generated - internally by the server on creation.""", + description="Unique ID to access the resource, generated " + "internally by the server on creation.", ) diff --git a/ansys/rep/client/jms/schema/evaluator.py b/ansys/rep/client/jms/schema/evaluator.py index 377de5d73..d1b4c5a51 100644 --- a/ansys/rep/client/jms/schema/evaluator.py +++ b/ansys/rep/client/jms/schema/evaluator.py @@ -19,8 +19,8 @@ class Meta: ordered = True host_id = fields.String( - description="""Unique identifier built from hardware information and - selected job_definition details of an evaluator.""" + description="Unique identifier built from hardware information and " + "selected job_definition details of an evaluator." ) name = fields.String(allow_none=True, description="Name of the evaluator.") hostname = fields.String( @@ -34,8 +34,8 @@ class Meta: ) project_server_select = fields.Bool( allow_none=True, - description="""Whether the evaluator allows server-driven assignment of projects or uses - it's own local settings.""", + description="Whether the evaluator allows server-driven assignment of projects or uses " + "it's own local settings.", ) alive_update_interval = fields.Int( allow_none=True, @@ -44,8 +44,8 @@ class Meta: update_time = fields.DateTime( allow_none=True, load_only=True, - description="""Last time the evaluator updated it's registration details. - Used to check which evaluators are alive.""", + description="Last time the evaluator updated it's registration details. " + "Used to check which evaluators are alive.", ) external_access_port = fields.Integer( allow_none=True, description="Port number for external access to the evaluator." @@ -60,6 +60,6 @@ class Meta: ) configuration = fields.Dict( allow_none=True, - description="""Details of the evaluator configuration, - including hardware info and available applications.""", + description="Details of the evaluator configuration, " + "including hardware info and available applications.", ) diff --git a/ansys/rep/client/jms/schema/file.py b/ansys/rep/client/jms/schema/file.py index adc3edad5..594db1a97 100644 --- a/ansys/rep/client/jms/schema/file.py +++ b/ansys/rep/client/jms/schema/file.py @@ -19,8 +19,8 @@ class Meta(ObjectSchema.Meta): name = fields.String(description="Name of the file resource.") type = fields.String( allow_none=True, - description="""Type of the file. This can be any string but using a correct media - type for the given resource is advisable.""", + description="Type of the file. This can be any string but using a correct media " + "type for the given resource is advisable.", ) storage_id = fields.String( allow_none=True, description="File's identifier in the (orthogonal) file storage system" @@ -43,8 +43,8 @@ class Meta(ObjectSchema.Meta): format = fields.String(allow_none=True) evaluation_path = fields.String( allow_none=True, - description="""Relative path under which the file instance for a - design point evaluation will be stored.""", + description="Relative path under which the file instance for a " + "design point evaluation will be stored.", ) monitor = fields.Bool( @@ -55,10 +55,10 @@ class Meta(ObjectSchema.Meta): ) collect_interval = fields.Int( allow_none=True, - description="""Collect frequency for a file with collect=True. - Min value limited by the evaluator's settings. - 0/None - let the evaluator decide, - other value - interval in seconds""", + description="Collect frequency for a file with collect=True." + " Min value limited by the evaluator's settings." + " 0/None - let the evaluator decide," + " other value - interval in seconds", ) reference_id = IdReference( diff --git a/ansys/rep/client/jms/schema/job.py b/ansys/rep/client/jms/schema/job.py index b56ce0ce0..9abc248a0 100644 --- a/ansys/rep/client/jms/schema/job.py +++ b/ansys/rep/client/jms/schema/job.py @@ -34,22 +34,22 @@ class Meta(ObjectSchema.Meta): allow_none=False, attribute="job_definition_id", referenced_class="JobDefinition", - description="""ID of the linked job_definition, - see :class:`ansys.rep.client.jms.JobDefinition`.""", + description="ID of the linked job_definition, " + "see :class:`ansys.rep.client.jms.JobDefinition`.", ) priority = fields.Integer( allow_none=True, default=0, - description="""Priority with which design points are evaluated. The default is 0, - which is the highest priority. Assigning a higher value to a design - point makes it a lower priority.""", + description="Priority with which design points are evaluated. The default is 0, " + "which is the highest priority. Assigning a higher value to a design " + "point makes it a lower priority.", ) values = fields.Dict( keys=fields.String(), allow_none=True, - description="""Dictionary with (name,value) pairs for all parameters defined in the - linked job_definition.""", + description="Dictionary with (name,value) pairs for all parameters defined in the " + "linked job_definition.", ) fitness = fields.Float(allow_none=True, description="Fitness value computed.") fitness_term_values = fields.Dict( @@ -64,8 +64,8 @@ class Meta(ObjectSchema.Meta): ) executed_task_definition_level = fields.Integer( allow_none=True, - description="""Execution level of the last executed - process step (-1 if none has been executed yet).""", + description="Execution level of the last executed " + "process step (-1 if none has been executed yet).", ) creation_time = fields.DateTime( diff --git a/ansys/rep/client/jms/schema/job_definition.py b/ansys/rep/client/jms/schema/job_definition.py index dacf67ac7..6dc397ff1 100644 --- a/ansys/rep/client/jms/schema/job_definition.py +++ b/ansys/rep/client/jms/schema/job_definition.py @@ -22,8 +22,8 @@ class Meta(ObjectSchema.Meta): name = fields.String(allow_none=True, description="Name of the job_definition") active = fields.Boolean( - description="""Defines whether this is the active job_definition in the - project where evaluators will evaluate pending design points""" + description="Defines whether this is the active job_definition in the " + "project where evaluators will evaluate pending design points" ) creation_time = fields.DateTime( allow_none=True, diff --git a/ansys/rep/client/jms/schema/operation.py b/ansys/rep/client/jms/schema/operation.py new file mode 100644 index 000000000..6e004ee01 --- /dev/null +++ b/ansys/rep/client/jms/schema/operation.py @@ -0,0 +1,20 @@ +from marshmallow import fields + +from .base import ObjectSchema + + +class OperationSchema(ObjectSchema): + class Meta(ObjectSchema.Meta): + pass + + name = fields.String(allow_none=True) + finished = fields.Bool(allow_none=True) + succeeded = fields.Bool(allow_none=True) + + progress = fields.Float(allow_none=True) + status = fields.String(allow_none=True) + result = fields.Dict(allow_none=True) + messages = fields.List(fields.Dict(), allow_none=True) + + start_time = fields.DateTime(allow_none=True) + end_time = fields.DateTime(allow_none=True) diff --git a/ansys/rep/client/jms/schema/parameter_definition.py b/ansys/rep/client/jms/schema/parameter_definition.py index 5fc73964d..911496a51 100644 --- a/ansys/rep/client/jms/schema/parameter_definition.py +++ b/ansys/rep/client/jms/schema/parameter_definition.py @@ -45,9 +45,9 @@ class Meta(ParameterDefinitionBaseSchema.Meta): upper_limit = fields.Float(allow_none=True, description="Upper bound for the parameter value.") step = fields.Float( allow_none=True, - description="""If provided, allowable values are given by: - AllowableValue = lower_limit + n * step, - where n is an integer and AllowableValue <= upper_limit.""", + description="If provided, allowable values are given by: " + "AllowableValue = lower_limit + n * step, " + "where n is an integer and AllowableValue <= upper_limit.", ) cyclic = fields.Bool(allow_none=True, description="Indicates if the parameter is cyclic.") value_list = fields.List( diff --git a/ansys/rep/client/jms/schema/parameter_mapping.py b/ansys/rep/client/jms/schema/parameter_mapping.py index 8266891d5..a8f4d5fdb 100644 --- a/ansys/rep/client/jms/schema/parameter_mapping.py +++ b/ansys/rep/client/jms/schema/parameter_mapping.py @@ -35,8 +35,8 @@ class Meta(ObjectSchema.Meta): allow_none=True, attribute="parameter_definition_id", referenced_class="ParameterDefinition", - description="""ID of the linked parameter definition, - see :class:`ansys.rep.client.jms.ParameterDefinition`.""", + description="ID of the linked parameter definition, " + "see :class:`ansys.rep.client.jms.ParameterDefinition`.", ) task_definition_property = fields.String(allow_none=True) file_id = IdReference( diff --git a/ansys/rep/client/jms/schema/selection.py b/ansys/rep/client/jms/schema/selection.py index 98242db0e..23be68cb4 100644 --- a/ansys/rep/client/jms/schema/selection.py +++ b/ansys/rep/client/jms/schema/selection.py @@ -12,7 +12,7 @@ from .object_reference import IdReference, IdReferenceList -class SelectionSchema(ObjectSchema): +class JobSelectionSchema(ObjectSchema): class Meta(ObjectSchema.Meta): pass @@ -31,7 +31,7 @@ class Meta(ObjectSchema.Meta): allow_none=True, attribute="algorithm_id", referenced_class="DesignExplorationAlgorithm", - description="""ID of the :class:`ansys.rep.client.jms.Algorithm` - the selection belongs to (optional).""", + description="ID of the :class:`ansys.rep.client.jms.Algorithm` " + "the selection belongs to (optional).", ) object_ids = IdReferenceList("Job", attribute="jobs", description="List of design point IDs.") diff --git a/ansys/rep/client/jms/schema/task.py b/ansys/rep/client/jms/schema/task.py index 4a138a0a7..4c9cf63a3 100644 --- a/ansys/rep/client/jms/schema/task.py +++ b/ansys/rep/client/jms/schema/task.py @@ -63,8 +63,8 @@ class Meta(ObjectSchema.Meta): task_definition_snapshot = fields.Nested( TaskDefinitionSchema, allow_none=True, - description="""Snapshot of :class:`ansys.rep.client.jms.TaskDefinition` - created when task status changes to prolog, before evaluation.""", + description="Snapshot of :class:`ansys.rep.client.jms.TaskDefinition` " + "created when task status changes to prolog, before evaluation.", ) job_id = IdReference( diff --git a/doc/source/auth.rst b/doc/source/api/auth.rst similarity index 58% rename from doc/source/auth.rst rename to doc/source/api/auth.rst index 65f93181c..86a34b5a4 100644 --- a/doc/source/auth.rst +++ b/doc/source/api/auth.rst @@ -1,19 +1,21 @@ Authentication Service =========================== -The DCS Authentication Service processes all DCS sign ins following OAuth 2.0 resource owner password credentials flow. -When you enter your DCS credentials, you get an access token (expiring after 24 hours) and a refresh token for authenticating all services. +--TODO Review-- -The Authentication Python client wraps around the Authentication Service REST API available at ``https://hostname/dcs/auth/api``. +The REP Authentication Service processes all REP sign ins following OAuth 2.0 resource owner password credentials flow. +When you enter your REP credentials, you get an access token (expiring after 24 hours) and a refresh token for authenticating all services. + +The `ansys.rep.client.auth` subpackage wraps around the Authentication Service REST API available at ``https://hostname/dcs/auth/api``. Authentication function ------------------------------------------ .. autofunction:: ansys.rep.client.auth.authenticate -Client object +Auth API ------------------------------------------ -.. autoclass:: ansys.rep.client.auth.Client +.. autoclass:: ansys.rep.client.auth.AuthApi :members: diff --git a/doc/source/exceptions.rst b/doc/source/api/exceptions.rst similarity index 89% rename from doc/source/exceptions.rst rename to doc/source/api/exceptions.rst index 14f9e4a04..7d1920fc4 100644 --- a/doc/source/exceptions.rst +++ b/doc/source/api/exceptions.rst @@ -6,7 +6,7 @@ HTTP requests returning an unsuccessful status code will raise: * :exc:`ansys.rep.client.ClientError` for client errors (4xx status code, e.g. bad syntax or not found) * :exc:`ansys.rep.client.APIError` for server errors (5xx status code, e.g. internal server error or not implemented) -All exceptions that the Ansys DCS clients explicitly raise inherit from :exc:`ansys.rep.client.REPError`. +All exceptions that the Ansys REP clients explicitly raise inherit from :exc:`ansys.rep.client.REPError`. .. autoexception:: ansys.rep.client.REPError :members: diff --git a/doc/source/api_reference.rst b/doc/source/api/index.rst similarity index 97% rename from doc/source/api_reference.rst rename to doc/source/api/index.rst index 4d25efaa4..4d8cc8341 100644 --- a/doc/source/api_reference.rst +++ b/doc/source/api/index.rst @@ -9,5 +9,5 @@ If you are looking for information on a specific function, class, or method, thi :maxdepth: 3 auth - dps + jms exceptions \ No newline at end of file diff --git a/doc/source/dps.rst b/doc/source/api/jms.rst similarity index 74% rename from doc/source/dps.rst rename to doc/source/api/jms.rst index 646f688a1..4c55d63a6 100644 --- a/doc/source/dps.rst +++ b/doc/source/api/jms.rst @@ -1,9 +1,9 @@ -Design Point Service +Job Management Service =========================== -Ansys DCS includes Ansys Design Point Service (DPS), which is the main service for storing and evaluating thousands of design points using multiple heterogeneous compute resources. +Ansys REP includes Job Management Service (JMS), which is the main service for storing and evaluating jobs using multiple heterogeneous compute resources. -The DPS Python client wraps around the DPS service REST API available at ``https://hostname/dcs/dps/api``. +The Python subpackage `ansys.rep.client.jms` wraps around the JMS service REST API available at ``https://hostname/rep/jms/api``. Connection module ------------------------------------------ @@ -11,11 +11,22 @@ Connection module :members: -Client object +Client object (TOMOVE) ------------------------------------ -.. autoclass:: ansys.rep.client.jms.Client +.. autoclass:: ansys.rep.client.Client :members: +JMS Api +------------------------------------ +.. autoclass:: ansys.rep.client.jms.JmsApi + :members: + :undoc-members: + +Project Api +------------------------------------ +.. autoclass:: ansys.rep.client.jms.ProjectApi + :members: + :undoc-members: File -------------------------------------- @@ -59,7 +70,7 @@ Parameters .. autoclass:: ansys.rep.client.jms.ParameterMapping :members: -Process Steps +Task Definition -------------------------------------- .. autoclass:: ansys.rep.client.jms.SuccessCriteria @@ -72,7 +83,7 @@ Process Steps :members: -JobDefinition +Job Definition -------------------------------------- .. autoclass:: ansys.rep.client.jms.JobDefinition @@ -87,17 +98,17 @@ Task :members: -Design Point +Job ------------------------------------------- .. autoclass:: ansys.rep.client.jms.Job :members: -Design Point Selection +Job Selection ---------------------------------------- -.. autoclass:: ansys.rep.client.jms.Selection +.. autoclass:: ansys.rep.client.jms.JobSelection :members: diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst deleted file mode 100644 index 0b112bb7d..000000000 --- a/doc/source/changelog.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _changelog: - -Changelog -========= - -.. toctree:: - :maxdepth: 1 - - changelog/v221 - changelog/v212 - diff --git a/doc/source/changelog/index.rst b/doc/source/changelog/index.rst new file mode 100644 index 000000000..c204b69ea --- /dev/null +++ b/doc/source/changelog/index.rst @@ -0,0 +1,7 @@ +.. _changelog: + +Changelog +========= + +.. toctree:: + :maxdepth: 1 \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index 697c9e1cd..cddda2fd2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -35,6 +35,7 @@ "sphinxcontrib.globalsubs", "sphinx.ext.intersphinx", "sphinx_copybutton", + "sphinxnotes.strike", ] @@ -135,17 +136,17 @@ #'canonical_url': '', #'analytics_id': 'UA-XXXXXXX-1', # Provided by Google in your dashboard #'logo_only': False, - "display_version": True, - "prev_next_buttons_location": "bottom", + # "display_version": True, + # "prev_next_buttons_location": "bottom", #'style_external_links': False, #'vcs_pageview_mode': '', #'style_nav_header_background': 'white', # Toc options "collapse_navigation": True, - "sticky_navigation": True, + # "sticky_navigation": True, "navigation_depth": 4, - "includehidden": True, - "titles_only": False, + # "includehidden": True, + # "titles_only": False, } # Add any paths that contain custom themes here, relative to this directory. diff --git a/doc/source/ex_adding_file.rst b/doc/source/examples/ex_adding_file.rst similarity index 98% rename from doc/source/ex_adding_file.rst rename to doc/source/examples/ex_adding_file.rst index 095aa636c..c21d4f3f1 100644 --- a/doc/source/ex_adding_file.rst +++ b/doc/source/examples/ex_adding_file.rst @@ -13,7 +13,7 @@ Here we show you how to modify the project in order to pick up the Mechanical `` To begin with, you need to copy the ``solve.out`` file to the directory of the project. To this end, we create two simple APDL command snippets as shown in the screenshot (the same is done for the radial local case). -.. image:: _static/bicycle_command_snippet.png +.. image:: ../_static/bicycle_command_snippet.png :alt: bicycle project command snippet Then, once the project is sent to the DCS server via Workbench, we can add the ``solve.out`` files to the list of collected output files using the Python client. diff --git a/doc/source/ex_download.rst b/doc/source/examples/ex_download.rst similarity index 100% rename from doc/source/ex_download.rst rename to doc/source/examples/ex_download.rst diff --git a/doc/source/ex_motorbike_frame.rst b/doc/source/examples/ex_motorbike_frame.rst similarity index 98% rename from doc/source/ex_motorbike_frame.rst rename to doc/source/examples/ex_motorbike_frame.rst index 57d006f25..280701ae8 100644 --- a/doc/source/ex_motorbike_frame.rst +++ b/doc/source/examples/ex_motorbike_frame.rst @@ -8,7 +8,7 @@ of a tubular steel trellis motorbike-frame. After creating the project job_definition, 10 design points with randomly chosen parameter values are created and set to pending. -.. image:: _static/motorbike_frame.jpg +.. image:: ../_static/motorbike_frame.jpg :scale: 50 % :align: center :alt: motorbike frame picture @@ -26,7 +26,7 @@ by U. M. Fasel, O. Koenig, M. Wintermantel and P. Ermanni. .. only:: builder_html - The project setup script as well as the data files can be downloaded here :download:`MAPDL Motorbike Frame Project <../mapdl_motorbike_frame.zip>`. + The project setup script as well as the data files can be downloaded here :download:`MAPDL Motorbike Frame Project <../../../mapdl_motorbike_frame.zip>`. We shall now dissect the project setup script in detail. diff --git a/doc/source/examples.rst b/doc/source/examples/index.rst similarity index 100% rename from doc/source/examples.rst rename to doc/source/examples/index.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index 4c097fc65..c229007b3 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,20 +1,17 @@ -Ansys DCS Python Client +Ansys REP Python Client ============================= -.. image:: _static/ansys_dcs_banner.jpg - - -Ansys Distributed Compute Services (DCS) is a family of applications that enables you to distribute, +Ansys Remote Execution Platform (REP) is a family of applications that enables you to distribute, manage and solve simulations on a variety of compute resources. -As part of this, design point services (DPS) facilitates the robust solution of +As part of this, the Job Management Service (JMS) facilitates the robust solution of tens of thousands of design points spread across clusters, networks and operating systems. -**ansys-dcs-client** brings Ansys DCS to your Python application. -Wrapping around the DCS REST APIs, it allows you to: +**ansys-rep-client** brings Ansys REP to your Python application. +Wrapping around the REP REST APIs, it allows you to: * create new projects and modify existing ones -* monitor and manage design points +* monitor and manage jobs * run your own design exploration algorithms * retrieve simulation results @@ -26,6 +23,6 @@ User Guide install quickstart - examples - api_reference - changelog \ No newline at end of file + examples/index + api/index + changelog/index \ No newline at end of file diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 93dca861e..0dcfdddfe 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -3,38 +3,40 @@ Quickstart =============== -This guide will walk you through the basics of interacting with a DCS server. More elaborated examples are available in the :ref:`Examples ` chapter, +This guide will walk you through the basics of interacting with a REP server. More elaborated examples are available in the :ref:`Examples ` chapter, while detailed documentation can be found in the :ref:`Code Documentation `. To reproduce the code samples provided below, you will need: -- A running DCS server, see the :ansys_dcs_help:`DPS Startup ` page in the ANSYS Help for detailed instructions. -- A Python shell with ``ansys-dcs-client`` installed. If you haven't installed it yet, please refer to the :ref:`Installation ` guide. +- A running REP server, see TODO :strike:`the DPS Startup page in the ANSYS Help for detailed instructions`. +- A Python shell with ``ansys-rep-client`` installed. If you haven't installed it yet, please refer to the :ref:`Installation ` guide. -Connect to a DCS Server +Connect to a REP Server -------------------------- -Let's start by connecting to a DCS server running on the localhost with default username and password. +Let's start by connecting to a REP server running on the localhost with default username and password. .. code-block:: python - from ansys.rep.client.jms import Client + from ansys.rep.client import Client + from ansys.rep.client.jms import JmsApi - client = Client(rep_url="https://127.0.0.1/dcs", username="repadmin", password="repadmin") + client = Client(rep_url="https://localhost:8443/rep", username="repadmin", password="repadmin") - # check which DCS version the server is running - print(client.get_api_info()['build']['external_version']) + # check which JMS version the server is running + jms_api = JmsApi(client) + print(jms_api.get_api_info()['build']['external_version']) # get all projects - projects = client.get_projects() + projects = jms_api.get_projects() Query projects statistics to find out how many design points are currently running .. code-block:: python - projects = client.get_projects(statistics=True) - num_running_dps = sum(p.statistics["eval_status"]["running"] for p in projects) + projects = jms_api.get_projects(statistics=True) + num_running_jobs = sum(p.statistics["eval_status"]["running"] for p in projects) Create a demo project: the MAPDL motorbike frame example --------------------------------------------------------- @@ -44,7 +46,7 @@ of a tubular steel trellis motorbike-frame. .. only:: builder_html - The project setup script as well as the data files can be downloaded here :download:`MAPDL Motorbike Frame Project <../mapdl_motorbike_frame.zip>`. + The project setup script as well as the data files can be downloaded here :download:`MAPDL Motorbike Frame Project <../../mapdl_motorbike_frame.zip>`. To create the project you only need to run the `project_setup` script: :: @@ -52,8 +54,8 @@ of a tubular steel trellis motorbike-frame. $ python path_to_download_folder\mapdl_motorbike_frame\project_setup.py .. note:: - By default, the script tries to connect to the DCS server running on the localhost with default username and password. - If your DCS server is hosted at a different URL or you want to specify different credentials, + By default, the script tries to connect to the REP server running on the localhost with default username and password. + If your REP server is hosted at a different URL or you want to specify different credentials, please adjust the script before running it. @@ -66,7 +68,7 @@ Most ``get`` functions support filtering by query parameters. .. code-block:: python - project = client.get_project(id="mapdl_motorbike_frame") + project = jms_api.get_project(id="mapdl_motorbike_frame") # Get all design points with all fields jobs = project.get_jobs() @@ -225,20 +227,22 @@ Users with admin rights (such as the default ``repadmin`` user) can create new u .. code-block:: python - from ansys.rep.client.auth import Client, User + from ansys.rep.client import Client + from ansys.rep.client.auth import AuthApi, User - auth_client = Client(rep_url="https://127.0.0.1/dcs/", username="repadmin", password="repadmin") + client = Client(rep_url="https://127.0.0.1/dcs/", username="repadmin", password="repadmin") + auth_api = AuthApi(client) # modify the default password of the repadmin user - default_user = auth_client.get_users()[0] + default_user = auth_api.get_users()[0] default_user.password = 'new_password' - auth_client.update_user(default_user) + auth_api.update_user(default_user) # create a new non-admin user new_user = User(username='test_user', password='dummy', email='test_user@test.com', fullname='Test User', is_admin=False) - new_user = auth_client.create_user(new_user) + new_user = auth_api.create_user(new_user) Exception handling diff --git a/examples/mapdl_motorbike_frame/project_query.py b/examples/mapdl_motorbike_frame/project_query.py index c75ce9e6e..2cda7dc59 100644 --- a/examples/mapdl_motorbike_frame/project_query.py +++ b/examples/mapdl_motorbike_frame/project_query.py @@ -10,8 +10,8 @@ import os from statistics import mean, stdev -from ansys.rep.client import REPError -from ansys.rep.client.jms import Client +from ansys.rep.client import Client, REPError +from ansys.rep.client.jms import JmsApi, ProjectApi log = logging.getLogger(__name__) @@ -27,32 +27,35 @@ def print_value_stats(values, title): def query_stats(client, project_name): """Query statistics.""" log.info("=== Query values and compoute statistics ===") + jms_api = JmsApi(client) log.info("=== Project") - project = client.get_project(name=project_name) + project = jms_api.get_project(name=project_name) log.info(f"ID: {project.id}") log.info(f"Created on: {project.creation_time}") + project_api = ProjectApi(client, project.id) + log.info("=== Query jobs") - jobs = project.get_jobs(eval_status="evaluated", fields=["id", "values", "elapsed_time"]) + jobs = project_api.get_jobs(eval_status="evaluated", fields=["id", "values", "elapsed_time"]) if not jobs: log.info("No evaluated jobs found to compute statistics from, exiting") return log.info(f"Statistics across {len(jobs)} jobs") - values = [dp.values["mapdl_elapsed_time_obtain_license"] for dp in jobs] + values = [job.values["mapdl_elapsed_time_obtain_license"] for job in jobs] print_value_stats( values, "=== License checkoput (parameter: mapdl_elapsed_time_obtain_license)" ) - values = [dp.values["mapdl_elapsed_time"] for dp in jobs] + values = [job.values["mapdl_elapsed_time"] for job in jobs] print_value_stats(values, "=== Elapsed time MAPDL (mapdl_elapsed_time)") - values = [dp.elapsed_time for dp in jobs] + values = [job.elapsed_time for job in jobs] print_value_stats(values, "=== Elapsed time REP (elapsed_time)") log.info("=== Query tasks") - tasks = project.get_tasks( + tasks = project_api.get_tasks( eval_status="evaluated", fields=["id", "prolog_time", "running_time", "finished_time"] ) log.info("Statistics across tasks") @@ -69,30 +72,34 @@ def download_files(client, project_name): out_path = os.path.join(os.path.dirname(__file__), "downloads") log.info(f"Downloading files to {out_path}") - project = client.get_project(name=project_name) + jms_api = JmsApi(client) + project = jms_api.get_project(name=project_name) # Todo: Fix needed in the backend: # Currently only the get_project() called with id returns all fields of project. - project = client.get_project(id=project.id) + project = jms_api.get_project(id=project.id) log.info(f"Project id: {project.id}") + project_api = ProjectApi(client, project.id) - jobs = project.get_jobs(eval_status="evaluated", fields=["id", "values", "elapsed_time"]) + jobs = project_api.get_jobs(eval_status="evaluated", fields=["id", "values", "elapsed_time"]) log.info(f"# evaluated jobs: {len(jobs)}") num = min(len(jobs), 10) - log.info(f"=== Example 1: Downloading output files of {num} jobs using File.download()") + log.info( + f"=== Example 1: Downloading output files of {num} jobs using ProjectApi.download_file()" + ) for job in jobs[0:num]: log.info(f"Job {job.id}") - for task in job.get_tasks(): + for task in project_api.get_tasks(job_id=job.id): log.info(f"Task {task.id}") - files = project.get_files(id=task.output_file_ids) + files = project_api.get_files(id=task.output_file_ids) for f in files: fpath = os.path.join(out_path, f"task_{task.id}") log.info(f"Download output file {f.evaluation_path} to {fpath}") - f.download(target_path=fpath) + project_api.download_file(file=f, target_path=fpath) num = 10 - files = project.get_files(type="image/jpeg", limit=num, content=True) + files = project_api.get_files(type="image/jpeg", limit=num, content=True) num = len(jobs) log.info(f"=== Example 2: Global download of {num} jpeg images in project using content=True") for f in files: diff --git a/examples/mapdl_motorbike_frame/project_setup.py b/examples/mapdl_motorbike_frame/project_setup.py index dba95ae29..4cb1a14e7 100644 --- a/examples/mapdl_motorbike_frame/project_setup.py +++ b/examples/mapdl_motorbike_frame/project_setup.py @@ -9,18 +9,19 @@ import os import random -from ansys.rep.client import REPError +from ansys.rep.client import Client, REPError from ansys.rep.client import __external_version__ as ansys_version from ansys.rep.client.jms import ( - Client, File, FitnessDefinition, FloatParameterDefinition, + JmsApi, Job, JobDefinition, Licensing, ParameterMapping, Project, + ProjectApi, ResourceRequirements, Software, StringParameterDefinition, @@ -43,9 +44,12 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): for Design Optimization of a Tubular Steel Trellis Motorbike-Frame", 2003 by U. M. Fasel, O. Koenig, M. Wintermantel and P. Ermanni. """ + jms_api = JmsApi(client) log.debug("=== Project") proj = Project(name=name, priority=1, active=True) - proj = client.create_project(proj, replace=True) + proj = jms_api.create_project(proj, replace=True) + + project_api = ProjectApi(client, proj.id) log.debug("=== Files") cwd = os.path.dirname(__file__) @@ -88,7 +92,7 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): ) ) - files = proj.create_files(files) + files = project_api.create_files(files) file_ids = {f.name: f.id for f in files} log.debug("=== JobDefinition with simulation workflow and parameters") @@ -108,7 +112,7 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): ] ) - float_input_params = proj.create_parameter_definitions(float_input_params) + float_input_params = project_api.create_parameter_definitions(float_input_params) param_mappings = [] pi = 0 for i in range(1, 4): @@ -137,7 +141,7 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): str_input_params.append( StringParameterDefinition(name="tube%s" % i, default="1", value_list=["1", "2", "3"]) ) - str_input_params = proj.create_parameter_definitions(str_input_params) + str_input_params = project_api.create_parameter_definitions(str_input_params) for i in range(1, 22): param_mappings.append( @@ -153,7 +157,7 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): output_params = [] for pname in ["weight", "torsion_stiffness", "max_stress"]: output_params.append(FloatParameterDefinition(name=pname)) - output_params = proj.create_parameter_definitions(output_params) + output_params = project_api.create_parameter_definitions(output_params) for pd in output_params: param_mappings.append( ParameterMapping( @@ -169,7 +173,7 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): stat_params.append(FloatParameterDefinition(name="mapdl_elapsed_time_obtain_license")) stat_params.append(FloatParameterDefinition(name="mapdl_cp_time")) stat_params.append(FloatParameterDefinition(name="mapdl_elapsed_time")) - stat_params = proj.create_parameter_definitions(stat_params) + stat_params = project_api.create_parameter_definitions(stat_params) param_mappings.append( ParameterMapping( @@ -288,8 +292,8 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): ) job_def.fitness_definition = fd - task_defs = proj.create_task_definitions(task_defs) - param_mappings = proj.create_parameter_mappings(param_mappings) + task_defs = project_api.create_task_definitions(task_defs) + param_mappings = project_api.create_parameter_mappings(param_mappings) job_def.parameter_definition_ids = [ pd.id for pd in float_input_params + str_input_params + output_params + stat_params @@ -298,9 +302,9 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): job_def.task_definition_ids = [td.id for td in task_defs] # Create job_definition in project - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] - job_def = proj.get_job_definitions()[0] + job_def = project_api.get_job_definitions()[0] log.debug(f"=== Create {num_jobs} jobs") jobs = [] @@ -310,8 +314,10 @@ def create_project(client, name, num_jobs=20, use_exec_script=False): for p in float_input_params } values.update({p.name: random.choice(p.value_list) for p in str_input_params}) - jobs.append(Job(name=f"Job.{i}", values=values, eval_status="pending")) - jobs = job_def.create_jobs(jobs) + jobs.append( + Job(name=f"Job.{i}", values=values, eval_status="pending", job_definition_id=job_def.id) + ) + jobs = project_api.create_jobs(jobs) log.info(f"Created project '{proj.name}', ID='{proj.id}'") diff --git a/examples/mapdl_tyre_performance/project_setup.py b/examples/mapdl_tyre_performance/project_setup.py index 988fd16e5..b556e867b 100644 --- a/examples/mapdl_tyre_performance/project_setup.py +++ b/examples/mapdl_tyre_performance/project_setup.py @@ -9,16 +9,17 @@ import os import random -from ansys.rep.client import REPError +from ansys.rep.client import Client, REPError from ansys.rep.client import __external_version__ as ansys_version from ansys.rep.client.jms import ( - Client, File, FloatParameterDefinition, + JmsApi, Job, JobDefinition, ParameterMapping, Project, + ProjectApi, ResourceRequirements, Software, TaskDefinition, @@ -27,7 +28,7 @@ log = logging.getLogger(__name__) -def main(client, name, num_jobs): +def main(client: Client, name: str, num_jobs: int): """ Create project with Ansys MAPDL tire simulation. @@ -35,8 +36,11 @@ def main(client, name, num_jobs): in the technology demonstration guide (td-57). """ log.debug("=== Project") + jms_api = JmsApi(client) proj = Project(name=name, display_name="MAPDL Tyre Performance", priority=1, active=True) - proj = client.create_project(proj, replace=True) + proj = jms_api.create_project(proj, replace=True) + + project_api = ProjectApi(client, proj.id) log.debug("=== Files") cwd = os.path.dirname(__file__) @@ -73,7 +77,7 @@ def main(client, name, num_jobs): File(name="err", evaluation_path="file*.err", type="text/plain", collect=True, monitor=True) ) - files = proj.create_files(files) + files = project_api.create_files(files) file_ids = {f.name: f.id for f in files} log.debug("=== JobDefinition with simulation workflow and parameters") @@ -109,7 +113,7 @@ def main(client, name, num_jobs): default=20.0, ), ] - input_params = proj.create_parameter_definitions(input_params) + input_params = project_api.create_parameter_definitions(input_params) param_mappings = [ ParameterMapping( @@ -145,7 +149,7 @@ def main(client, name, num_jobs): FloatParameterDefinition(name="mapdl_cp_time", display_text="MAPDL CP Time"), FloatParameterDefinition(name="mapdl_elapsed_time", display_text="MAPDL Elapsed Time"), ] - output_params = proj.create_parameter_definitions(output_params) + output_params = project_api.create_parameter_definitions(output_params) param_mappings.extend( [ @@ -163,7 +167,7 @@ def main(client, name, num_jobs): ), ] ) - param_mappings = proj.create_parameter_mappings(param_mappings) + param_mappings = project_api.create_parameter_mappings(param_mappings) # TODO, add more @@ -183,7 +187,7 @@ def main(client, name, num_jobs): input_file_ids=[f.id for f in files[:2]], output_file_ids=[f.id for f in files[2:]], ) - task_def = proj.create_task_definitions([task_def])[0] + task_def = project_api.create_task_definitions([task_def])[0] # Create job_definition in project job_def = JobDefinition(name="JobDefinition.1", active=True) @@ -191,10 +195,10 @@ def main(client, name, num_jobs): job_def.task_definition_ids = [task_def.id] job_def.parameter_definition_ids = [pd.id for pd in params] job_def.parameter_mapping_ids = [pm.id for pm in param_mappings] - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] # Refresh the parameters - params = proj.get_parameter_definitions(id=job_def.parameter_definition_ids) + params = project_api.get_parameter_definitions(id=job_def.parameter_definition_ids) log.debug("=== Jobs") jobs = [] @@ -204,8 +208,10 @@ def main(client, name, num_jobs): for p in params if p.mode == "input" } - jobs.append(Job(name=f"Job.{i}", values=values, eval_status="pending")) - jobs = job_def.create_jobs(jobs) + jobs.append( + Job(name=f"Job.{i}", values=values, eval_status="pending", job_definition_id=job_def.id) + ) + jobs = project_api.create_jobs(jobs) log.info(f"Created project '{proj.name}', ID='{proj.id}'") diff --git a/examples/python_linked_multi_process_step/project_setup.py b/examples/python_linked_multi_process_step/project_setup.py index 6daaab6c9..a0880d52c 100644 --- a/examples/python_linked_multi_process_step/project_setup.py +++ b/examples/python_linked_multi_process_step/project_setup.py @@ -8,15 +8,16 @@ import os import random -from ansys.rep.client import REPError +from ansys.rep.client import Client, REPError from ansys.rep.client.jms import ( - Client, File, FloatParameterDefinition, + JmsApi, Job, JobDefinition, ParameterMapping, Project, + ProjectApi, ResourceRequirements, Software, SuccessCriteria, @@ -35,7 +36,9 @@ def main(client, num_task_definitions, num_jobs, start, inactive): priority=1, active=not inactive, ) - proj = client.create_project(proj, replace=True) + jms_api = JmsApi(client) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) log.debug("=== Files") cwd = os.path.dirname(__file__) @@ -70,7 +73,7 @@ def main(client, num_task_definitions, num_jobs, start, inactive): ) ) - files = proj.create_files(files) + files = project_api.create_files(files) file_ids = {f.name: f.id for f in files} log.debug("=== JobDefinition with simulation workflow and parameters") @@ -83,7 +86,7 @@ def main(client, num_task_definitions, num_jobs, start, inactive): params.extend( [FloatParameterDefinition(name=f"product{i}") for i in range(num_task_definitions)] ) - params = proj.create_parameter_definitions(params) + params = project_api.create_parameter_definitions(params) job_def.parameter_definition_ids = [o.id for o in params] param_mappings = [ @@ -105,7 +108,7 @@ def main(client, num_task_definitions, num_jobs, start, inactive): for i in range(num_task_definitions) ] ) - param_mappings = proj.create_parameter_mappings(param_mappings) + param_mappings = project_api.create_parameter_mappings(param_mappings) job_def.parameter_mapping_ids = [o.id for o in param_mappings] log.debug("=== Process Steps") @@ -145,14 +148,14 @@ def main(client, num_task_definitions, num_jobs, start, inactive): success_criteria=SuccessCriteria(return_code=0, require_all_output_files=True), ) ) - task_defs = proj.create_task_definitions(task_defs) + task_defs = project_api.create_task_definitions(task_defs) job_def.task_definition_ids = [o.id for o in task_defs] # Create job_definition in project - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] # Refresh parameter definitions - params = proj.get_parameter_definitions(job_def.parameter_definition_ids) + params = project_api.get_parameter_definitions(job_def.parameter_definition_ids) log.debug("=== Design points") jobs = [] @@ -164,8 +167,10 @@ def main(client, num_task_definitions, num_jobs, start, inactive): values[p.name] = float( int(p.lower_limit + random.random() * (p.upper_limit - p.lower_limit)) ) - jobs.append(Job(name=f"Job.{i}", values=values, eval_status="pending")) - jobs = job_def.create_jobs(jobs) + jobs.append( + Job(name=f"Job.{i}", values=values, eval_status="pending", job_definition_id=job_def.id) + ) + jobs = project_api.create_jobs(jobs) log.info(f"Created project '{proj.name}', ID='{proj.id}'") diff --git a/examples/python_multi_process_step/project_setup.py b/examples/python_multi_process_step/project_setup.py index 589796577..5c4e110db 100644 --- a/examples/python_multi_process_step/project_setup.py +++ b/examples/python_multi_process_step/project_setup.py @@ -27,15 +27,16 @@ from task_files import update_task_files -from ansys.rep.client import REPError +from ansys.rep.client import Client, REPError from ansys.rep.client.jms import ( - Client, File, IntParameterDefinition, + JmsApi, Job, JobDefinition, ParameterMapping, Project, + ProjectApi, ResourceRequirements, Software, StringParameterDefinition, @@ -70,7 +71,9 @@ def main( priority=1, active=not inactive, ) - proj = client.create_project(proj, replace=True) + jms_api = JmsApi(client) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) log.debug("=== Files") @@ -127,7 +130,7 @@ def main( ) ) - files = proj.create_files(files) + files = project_api.create_files(files) file_ids = {f.name: f.id for f in files} log.debug("=== JobDefinition with simulation workflow and parameters") @@ -151,8 +154,8 @@ def main( default='"orange"', ), ] - int_params = proj.create_parameter_definitions(int_params) - str_params = proj.create_parameter_definitions(str_params) + int_params = project_api.create_parameter_definitions(int_params) + str_params = project_api.create_parameter_definitions(str_params) params.extend(int_params + str_params) input_file_id = file_ids[f"td{i}_input"] @@ -192,7 +195,7 @@ def main( ) ) - mappings = proj.create_parameter_mappings(mappings) + mappings = project_api.create_parameter_mappings(mappings) log.debug("=== Task definitions") task_defs = [] @@ -226,16 +229,16 @@ def main( ) ) - task_defs = proj.create_task_definitions(task_defs) + task_defs = project_api.create_task_definitions(task_defs) # Create job_definition in project job_def.parameter_definition_ids = [o.id for o in params] job_def.parameter_mapping_ids = [o.id for o in mappings] job_def.task_definition_ids = [o.id for o in task_defs] - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] # Refresh param definitions - params = proj.get_parameter_definitions(id=job_def.parameter_definition_ids) + params = project_api.get_parameter_definitions(id=job_def.parameter_definition_ids) log.debug("=== Design points") jobs = [] @@ -249,8 +252,10 @@ def main( values[p.name] = int( p.lower_limit + random.random() * (p.upper_limit - p.lower_limit) ) - jobs.append(Job(name=f"Job.{i}", values=values, eval_status="pending")) - jobs = job_def.create_jobs(jobs) + jobs.append( + Job(name=f"Job.{i}", values=values, eval_status="pending", job_definition_id=job_def.id) + ) + jobs = project_api.create_jobs(jobs) # change dp task files if change_job_tasks > 0: diff --git a/examples/python_multi_process_step/task_files.py b/examples/python_multi_process_step/task_files.py index f1c40264e..1b1b25f50 100644 --- a/examples/python_multi_process_step/task_files.py +++ b/examples/python_multi_process_step/task_files.py @@ -68,12 +68,12 @@ def update_eval_script(task): return file.name -def update_task_files(proj, num_jobs, write_images): +def update_task_files(project_api, num_jobs, write_images): log.debug("=== Update Task files ===") cwd = os.path.dirname(__file__) - config = proj.get_job_definitions()[0] + config = project_api.get_job_definitions()[0] jobs = config.get_jobs(limit=num_jobs) for dp in jobs: @@ -139,7 +139,7 @@ def update_task_files(proj, num_jobs, write_images): ) ) - files = proj.create_files(files) + files = project_api.create_files(files) file_ids = {f.name: f.id for f in files} output_file_ids = [file_ids[new_out_name]] @@ -149,7 +149,7 @@ def update_task_files(proj, num_jobs, write_images): task.output_file_ids = output_file_ids task.input_file_ids = [file_ids[new_input_name], file_ids[new_eval_name]] - proj.update_tasks(tasks) + project_api.update_tasks(tasks) - proj.update_jobs(jobs) + project_api.update_jobs(jobs) log.info("Updated {} design points".format(len(jobs))) diff --git a/examples/python_two_bar_truss_problem/project_setup.py b/examples/python_two_bar_truss_problem/project_setup.py index 98aa84fc8..598520110 100644 --- a/examples/python_two_bar_truss_problem/project_setup.py +++ b/examples/python_two_bar_truss_problem/project_setup.py @@ -8,16 +8,17 @@ import os import random -from ansys.rep.client import REPError +from ansys.rep.client import Client, REPError from ansys.rep.client.jms import ( - Client, File, FitnessDefinition, FloatParameterDefinition, + JmsApi, Job, JobDefinition, ParameterMapping, Project, + ProjectApi, ResourceRequirements, Software, TaskDefinition, @@ -37,7 +38,9 @@ def main(client, num_jobs, use_exec_script): proj = Project( name="two_bar_truss_problem", display_name="Two-bar Truss Problem", priority=1, active=True ) - proj = client.create_project(proj, replace=True) + jms_api = JmsApi(client) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) log.debug("=== Files") cwd = os.path.dirname(__file__) @@ -79,7 +82,7 @@ def main(client, num_jobs, use_exec_script): ) ) - files = proj.create_files(files) + files = project_api.create_files(files) file_ids = {f.name: f.id for f in files} log.debug("=== JobDefinition with simulation workflow and parameters") @@ -112,7 +115,7 @@ def main(client, num_jobs, use_exec_script): name="load", lower_limit=1e1, upper_limit=1e5, default=66e3, units="lbs" ), ] - input_params = proj.create_parameter_definitions(input_params) + input_params = project_api.create_parameter_definitions(input_params) mappings = [ ParameterMapping( @@ -165,7 +168,7 @@ def main(client, num_jobs, use_exec_script): FloatParameterDefinition(name="buckling_stress", units="ksi"), FloatParameterDefinition(name="deflection", units="in"), ] - output_params = proj.create_parameter_definitions(output_params) + output_params = project_api.create_parameter_definitions(output_params) mappings.extend( [ @@ -196,7 +199,7 @@ def main(client, num_jobs, use_exec_script): ] ) - mappings = proj.create_parameter_mappings(mappings) + mappings = project_api.create_parameter_mappings(mappings) job_def.parameter_definition_ids = [o.id for o in input_params + output_params] job_def.parameter_mapping_ids = [o.id for o in mappings] @@ -220,7 +223,7 @@ def main(client, num_jobs, use_exec_script): task_def.execution_command = None task_def.execution_script_id = file_ids["exec_python"] - task_def = proj.create_task_definitions([task_def])[0] + task_def = project_api.create_task_definitions([task_def])[0] job_def.task_definition_ids = [task_def.id] # Fitness definition @@ -246,9 +249,9 @@ def main(client, num_jobs, use_exec_script): job_def.fitness_definition = fd # Create job_definition in project - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] - params = proj.get_parameter_definitions(job_def.parameter_definition_ids) + params = project_api.get_parameter_definitions(job_def.parameter_definition_ids) log.debug("=== Jobs") jobs = [] @@ -258,8 +261,10 @@ def main(client, num_jobs, use_exec_script): for p in params if p.mode == "input" } - jobs.append(Job(name=f"Job.{i}", values=values, eval_status="pending")) - jobs = job_def.create_jobs(jobs) + jobs.append( + Job(name=f"Job.{i}", values=values, eval_status="pending", job_definition_id=job_def.id) + ) + jobs = project_api.create_jobs(jobs) log.info(f"Created project '{proj.name}', ID='{proj.id}'") diff --git a/prepare_documentation.py b/prepare_documentation.py index 94798b872..1938a01fc 100644 --- a/prepare_documentation.py +++ b/prepare_documentation.py @@ -26,12 +26,13 @@ IntParameterDefinition, Job, JobDefinition, + JobSelection, LicenseContext, Licensing, + Operation, ParameterMapping, Project, ProjectPermission, - Selection, StringParameterDefinition, SuccessCriteria, Task, @@ -57,7 +58,7 @@ def custom_field_attributes(self, field, **kwargs): def generate_openapi_specs(): """Auto-generate schemas documentation in JSON format.""" - tgt_dir = os.path.join("doc", "schemas") + tgt_dir = os.path.join("doc", "source", "api", "schemas") if not os.path.exists(tgt_dir): os.makedirs(tgt_dir) @@ -67,7 +68,7 @@ def generate_openapi_specs(): LicenseContext, Job, Algorithm, - Selection, + JobSelection, JobDefinition, ParameterMapping, FloatParameterDefinition, @@ -84,6 +85,7 @@ def generate_openapi_specs(): Licensing, TaskDefinitionTemplate, User, + Operation, ]: ma_plugin = MarshmallowPlugin() @@ -113,6 +115,10 @@ def generate_openapi_specs(): else: modified_prop_dict[k] = v + # User.is_admin is Function field which doesn't get the type + if k == "is_admin": + v["type"] = "boolean" + with open(f"{os.path.join(tgt_dir, object_name)}.json", "w") as outfile: outfile.write(json.dumps({"properties": modified_prop_dict}, indent=4)) diff --git a/requirements/requirements_doc.txt b/requirements/requirements_doc.txt index 1e63c75dc..9c041f397 100644 --- a/requirements/requirements_doc.txt +++ b/requirements/requirements_doc.txt @@ -5,4 +5,5 @@ sphinx-copybutton==0.5 apispec==5.2.2 sphinxcontrib-httpdomain==1.8.0 sphinxcontrib-jsonschema==0.9.3 -sphinxcontrib-globalsubs==0.1.0 \ No newline at end of file +sphinxcontrib-globalsubs==0.1.0 +sphinxnotes-strike==1.1 \ No newline at end of file diff --git a/tests/auth/test_client.py b/tests/auth/test_api.py similarity index 80% rename from tests/auth/test_client.py rename to tests/auth/test_api.py index 52aaccf73..b104feb80 100644 --- a/tests/auth/test_client.py +++ b/tests/auth/test_api.py @@ -7,7 +7,8 @@ # ---------------------------------------------------------- import logging -from ansys.rep.client.auth import Client, User +from ansys.rep.client import Client +from ansys.rep.client.auth import AuthApi, User from tests.rep_test import REPTestCase log = logging.getLogger(__name__) @@ -16,8 +17,8 @@ class AuthClientTest(REPTestCase): def test_auth_client(self): - cl = Client(self.rep_url, username=self.username, password=self.password) - users = cl.get_users() + api = AuthApi(Client(self.rep_url, username=self.username, password=self.password)) + users = api.get_users() # we run the test only if self.username is an admin user for user in users: @@ -32,7 +33,7 @@ def test_auth_client(self): first_name="Test", last_name="User", ) - new_user = cl.create_user(new_user) + new_user = api.create_user(new_user) self.assertEqual(new_user.username, username) self.assertEqual(new_user.first_name, "Test") @@ -41,15 +42,15 @@ def test_auth_client(self): new_user.email = "update_email@test.com" new_user.last_name = "Smith" - cl.update_user(new_user) + api.update_user(new_user) self.assertEqual(new_user.username, username) self.assertEqual(new_user.first_name, "Test") self.assertEqual(new_user.last_name, "Smith") self.assertEqual(new_user.email, "update_email@test.com") - cl.delete_user(new_user) + api.delete_user(new_user) - users = cl.get_users() + users = api.get_users() usernames = [x.username for x in users] self.assertNotIn(new_user.username, usernames) diff --git a/tests/jms/test_algorithms.py b/tests/jms/test_algorithms.py index a00060d6e..66e512620 100644 --- a/tests/jms/test_algorithms.py +++ b/tests/jms/test_algorithms.py @@ -10,7 +10,8 @@ from marshmallow.utils import missing -from ansys.rep.client.jms.resource import Algorithm, Job, JobDefinition, Project, Selection +from ansys.rep.client.jms import JmsApi, ProjectApi +from ansys.rep.client.jms.resource import Algorithm, Job, JobDefinition, JobSelection, Project from tests.rep_test import REPTestCase log = logging.getLogger(__name__) @@ -20,31 +21,33 @@ class AlgorithmsTest(REPTestCase): def test_algorithms(self): log.debug("=== Client ===") - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"rep_client_test_jms_AlgorithmsTest_{self.run_id}" proj = Project(name=proj_name, active=True) - proj = client.create_project(proj, replace=True) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) job_def = JobDefinition(name="New Config", active=True) - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] # Create some Jobs jobs = [ Job(name=f"dp_{i}", eval_status="inactive", job_definition_id=job_def.id) for i in range(10) ] - jobs = proj.create_jobs(jobs) + jobs = project_api.create_jobs(jobs) # Create selections with some jobs - sels = [Selection(name="selection_0", jobs=[dp.id for dp in jobs[0:5]])] - sels.append(Selection(name="selection_1", jobs=[dp.id for dp in jobs[5:]])) + sels = [JobSelection(name="selection_0", jobs=[dp.id for dp in jobs[0:5]])] + sels.append(JobSelection(name="selection_1", jobs=[dp.id for dp in jobs[5:]])) for sel in sels: self.assertEqual(len(sel.jobs), 5) self.assertEqual(sel.algorithm_id, missing) - proj.create_selections(sels) - sels = proj.get_selections(fields="all") + project_api.create_job_selections(sels) + sels = project_api.get_job_selections(fields="all") for sel in sels: self.assertEqual(len(sel.jobs), 5) self.assertEqual(sel.algorithm_id, None) @@ -55,46 +58,46 @@ def test_algorithms(self): self.assertEqual(algo.description, missing) self.assertEqual(algo.jobs, missing) - algo = proj.create_algorithms([algo])[0] + algo = project_api.create_algorithms([algo])[0] self.assertEqual(len(algo.jobs), 0) self.assertEqual(algo.data, None) self.assertEqual(algo.description, None) # Link jobs to algorithm algo.jobs = [j.id for j in jobs] - algo = proj.update_algorithms([algo])[0] + algo = project_api.update_algorithms([algo])[0] self.assertEqual(len(algo.jobs), 10) # Link selections to algorithm for sel in sels: sel.algorithm_id = algo.id - sels = proj.update_selections(sels) + sels = project_api.update_job_selections(sels) # Query algorithm selections - sels = algo.get_selections() + sels = project_api.get_job_selections(algorithm_id=algo.id) for sel in sels: self.assertEqual(len(sel.jobs), 5) # Query algorithm design points - jobs = algo.get_jobs() + jobs = project_api.get_jobs(algorithm_id=algo.id) self.assertEqual(len(jobs), 10) # Update algorithm algo.description = "testing algorithm" algo.data = "data" algo_id = algo.id - proj.update_algorithms([algo]) - algo = proj.get_algorithms(id=algo_id)[0] + project_api.update_algorithms([algo]) + algo = project_api.get_algorithms(id=algo_id)[0] self.assertEqual(algo.description, "testing algorithm") self.assertEqual(algo.data, "data") # Delete some design points job_ids = [jobs[0].id, jobs[1].id, jobs[6].id, jobs[7].id] - proj.delete_jobs([Job(id=f"{j_id}") for j_id in job_ids]) - self.assertEqual(len(proj.get_jobs()), 6) + project_api.delete_jobs([Job(id=f"{j_id}") for j_id in job_ids]) + self.assertEqual(len(project_api.get_jobs()), 6) # Delete project - client.delete_project(proj) + jms_api.delete_project(proj) if __name__ == "__main__": diff --git a/tests/jms/test_client.py b/tests/jms/test_client.py deleted file mode 100644 index 93c82a794..000000000 --- a/tests/jms/test_client.py +++ /dev/null @@ -1,105 +0,0 @@ -# ---------------------------------------------------------- -# Copyright (C) 2019 by -# ANSYS Switzerland GmbH -# www.ansys.com -# -# Author(s): O.Koenig -# ---------------------------------------------------------- -import logging -import time -import unittest - -from ansys.rep.client.jms import Client -from ansys.rep.client.jms.resource import Job, Project -from tests.rep_test import REPTestCase - -log = logging.getLogger(__name__) - - -class REPClientTest(REPTestCase): - def test_authentication_workflows(self): - - client0 = Client(self.rep_url, self.username, self.password) - - self.assertTrue(client0.access_token is not None) - self.assertTrue(client0.refresh_token is not None) - - access_token0 = client0.access_token - refresh_token0 = client0.refresh_token - - # wait a second otherwise the OAuth server will issue the very same tokens - time.sleep(1) - - client0.refresh_access_token() - self.assertNotEqual(client0.access_token, access_token0) - self.assertNotEqual(client0.refresh_token, refresh_token0) - - client1 = Client(self.rep_url, access_token=client0.access_token) - self.assertEqual(client1.access_token, client0.access_token) - self.assertTrue(client1.refresh_token is None) - - client2 = Client(self.rep_url, refresh_token=client0.refresh_token) - self.assertTrue(client2.access_token is not None) - self.assertNotEqual(client2.refresh_token, client0.refresh_token) - client2.refresh_access_token() - - def test_client(self): - - # This test assumes that the project mapdl_motorbike_frame already exists on the DCS server. - # In case, you can create such project running the script - # examples/mapdl_motorbike_frame/project_setup.py - - log.debug("=== Client ===") - client = self.jms_client() - proj_name = "mapdl_motorbike_frame" - - log.debug("=== Projects ===") - projects = client.get_projects() - log.debug(f"Projects: {[p.id for p in projects]}") - project = None - for p in projects: - if p.name == proj_name: - project = p - - if project: - log.debug(f"Project: {project.id}") - log.debug(f"project={project}") - - new_proj = Project(name="New project", active=True) - new_proj = client.create_project(new_proj, replace=True) - # Delete project again - client.delete_project(new_proj) - - log.debug("=== JobDefinitions ===") - job_definitions = project.get_job_definitions(active=True) - job_def = job_definitions[0] - log.debug(f"job_definition={job_def}") - - log.debug("=== Design Points ===") - all_jobs = project.get_jobs(fields="all") - log.debug(f"# design points: {len(all_jobs)}") - if all_jobs: - log.debug(f"dp0={all_jobs[0]}") - - pending_jobs = project.get_jobs(eval_status="pending") - log.debug(f"Pending design points: {[j.id for j in pending_jobs]}") - - # Alternative access with manually instantiated project - proj = client.get_projects(name=proj_name)[0] - evaluated_jobs = proj.get_jobs(eval_status="evaluated", fields="all") - log.debug(f"Evaluated design points: {[j.id for j in evaluated_jobs]}") - - # Access design point data without objects - dp_data = project.get_jobs(limit=3, as_objects=False) - log.debug(f"dp_data={dp_data}") - - # Create some Jobs - new_jobs = [Job(name=f"new_dp_{i}", eval_status="pending") for i in range(10)] - created_jobs = job_def.create_jobs(new_jobs) - - # Delete Jobs again - job_def.delete_jobs(created_jobs) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/jms/test_evaluators.py b/tests/jms/test_evaluators.py index fe5aa0c7a..5c1a92274 100644 --- a/tests/jms/test_evaluators.py +++ b/tests/jms/test_evaluators.py @@ -11,6 +11,7 @@ from marshmallow.utils import missing +from ansys.rep.client.jms import JmsApi from ansys.rep.client.jms.schema.evaluator import EvaluatorSchema from tests.rep_test import REPTestCase @@ -53,17 +54,18 @@ def test_evaluator_deserialization(self): def test_evaluator_integration(self): - client = self.jms_client() - evaluators = client.get_evaluators() + client = self.client() + jms_api = JmsApi(client) + evaluators = jms_api.get_evaluators() if evaluators: self.assertTrue(evaluators[0].id is not None) - evaluators = client.get_evaluators(as_objects=False) + evaluators = jms_api.get_evaluators(as_objects=False) if evaluators: self.assertTrue(evaluators[0]["id"] is not None) - evaluators = client.get_evaluators(as_objects=False, fields=["hostname", "platform"]) + evaluators = jms_api.get_evaluators(as_objects=False, fields=["hostname", "platform"]) log.info(f"evaluators={evaluators}") if evaluators: self.assertTrue("hostname" in evaluators[0].keys()) @@ -71,7 +73,7 @@ def test_evaluator_integration(self): self.assertTrue("project_server_select" not in evaluators[0].keys()) # self.assertEqual(len(evaluators[0].keys()), 2) - evaluators = client.get_evaluators(fields=["hostname", "platform", "configuration"]) + evaluators = jms_api.get_evaluators(fields=["hostname", "platform", "configuration"]) if evaluators: i = next(i for i, e in enumerate(evaluators) if e.configuration) diff --git a/tests/jms/test_files.py b/tests/jms/test_files.py index 91edfddd7..b399170f6 100644 --- a/tests/jms/test_files.py +++ b/tests/jms/test_files.py @@ -12,6 +12,7 @@ from marshmallow.utils import missing +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import File, Project from tests.rep_test import REPTestCase @@ -21,10 +22,12 @@ class FilesTest(REPTestCase): def test_files(self): - client = self.jms_client() - proj = client.create_project( + client = self.client() + jms_api = JmsApi(client) + proj = jms_api.create_project( Project(name=f"rep_client_test_jms_FilesTest_{self.run_id}", active=False), replace=True ) + project_api = ProjectApi(client, proj.id) cwd = os.path.dirname(__file__) example_dir = os.path.join(cwd, "..", "..", "examples", "mapdl_motorbike_frame") @@ -47,10 +50,10 @@ def test_files(self): ) files.append(File(name="img", evaluation_path="file000.jpg", type="image/jpeg", hash=None)) files.append(File(name="out", evaluation_path="file.out", type="text/plain", hash=None)) - files_created = proj.create_files(files) + files_created = project_api.create_files(files) # Get files - files_queried = proj.get_files(content=True) + files_queried = project_api.get_files(content=True) # Compare file objects, comparing all attrs that are not missing on created file object attrs = [attr for attr in files[0].declared_fields() if getattr(files[0], attr) != missing] @@ -71,16 +74,16 @@ def test_files(self): with tempfile.TemporaryDirectory() as tpath: # test chunked file download - fpath = files_queried[0].download(tpath) + fpath = project_api.download_file(files_queried[0], tpath) with open(mac_path, "rb") as f, open(fpath, "rb") as sf: self.assertEqual(f.read(), sf.read()) - fpath = files_queried[1].download(tpath) + fpath = project_api.download_file(files_queried[1], tpath) with open(res_path, "rb") as f, open(fpath, "rb") as sf: self.assertEqual(f.read(), sf.read()) # test download without streaming - fpath = files_queried[0].download(tpath, stream=False) + fpath = project_api.download_file(files_queried[0], tpath, stream=False) with open(mac_path, "rb") as f, open(fpath, "rb") as sf: self.assertEqual(f.read(), sf.read()) @@ -88,12 +91,12 @@ def test_files(self): handler = lambda current_size: print( f"{current_size*1.0/files_queried[0].size * 100.0}% completed" ) - fpath = files_queried[0].download(tpath, progress_handler=handler) + fpath = project_api.download_file(files_queried[0], tpath, progress_handler=handler) with open(mac_path, "rb") as f, open(fpath, "rb") as sf: self.assertEqual(f.read(), sf.read()) # Delete project again - client.delete_project(proj) + jms_api.delete_project(proj) if __name__ == "__main__": diff --git a/tests/jms/test_fitness_definition.py b/tests/jms/test_fitness_definition.py index 48fec8c1f..43c9e2e13 100644 --- a/tests/jms/test_fitness_definition.py +++ b/tests/jms/test_fitness_definition.py @@ -10,6 +10,7 @@ from marshmallow.utils import missing +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import JobDefinition, Project from ansys.rep.client.jms.resource.fitness_definition import ( FitnessDefinition, @@ -122,22 +123,24 @@ def test_fitness_definition_serialization(self): def test_fitness_definition_integration(self): - client = self.jms_client() + client = self.client() proj_name = f"test_dps_FitnessDefinitionTest_{self.run_id}" proj = Project(name=proj_name, active=True) - proj = client.create_project(proj, replace=True) + jms_api = JmsApi(client) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) fd = FitnessDefinition(error_fitness=3.14) job_def = JobDefinition(name="New Config", active=True) job_def.fitness_definition = fd - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] self.assertEqual(job_def.fitness_definition.error_fitness, 3.14) job_def.fitness_definition.add_fitness_term( name="test_ftd", type="design_objective", expression="0.0", weighting_factor=1.0 ) - job_def = proj.update_job_definitions([job_def])[0] + job_def = project_api.update_job_definitions([job_def])[0] self.assertEqual(job_def.fitness_definition.fitness_term_definitions[0].name, "test_ftd") self.assertEqual( job_def.fitness_definition.fitness_term_definitions[0].type, "design_objective" @@ -147,7 +150,7 @@ def test_fitness_definition_integration(self): job_def.fitness_definition.add_fitness_term( name="another_term", type="target_constraint", weighting_factor=0.2, expression="2.0" ) - job_def = proj.update_job_definitions([job_def])[0] + job_def = project_api.update_job_definitions([job_def])[0] self.assertEqual(len(job_def.fitness_definition.fitness_term_definitions), 2) self.assertEqual( job_def.fitness_definition.fitness_term_definitions[1].name, "another_term" @@ -161,7 +164,7 @@ def test_fitness_definition_integration(self): ) # Delete project - client.delete_project(proj) + jms_api.delete_project(proj) if __name__ == "__main__": diff --git a/tests/jms/test_jms_api.py b/tests/jms/test_jms_api.py new file mode 100644 index 000000000..7150ccd99 --- /dev/null +++ b/tests/jms/test_jms_api.py @@ -0,0 +1,84 @@ +# ---------------------------------------------------------- +# Copyright (C) 2019 by +# ANSYS Switzerland GmbH +# www.ansys.com +# +# Author(s): O.Koenig +# ---------------------------------------------------------- +import logging +import unittest + +from ansys.rep.client.jms import JmsApi, ProjectApi +from ansys.rep.client.jms.resource import Job, Project +from tests.rep_test import REPTestCase + +log = logging.getLogger(__name__) + + +class REPClientTest(REPTestCase): + def test_jms_api(self): + + # This test assumes that the project mapdl_motorbike_frame already exists on the DCS server. + # In case, you can create such project running the script + # examples/mapdl_motorbike_frame/project_setup.py + + log.debug("=== Client ===") + client = self.client() + proj_name = "mapdl_motorbike_frame" + + log.debug("=== Projects ===") + jms_api = JmsApi(client) + projects = jms_api.get_projects() + log.debug(f"Projects: {[p.id for p in projects]}") + project = None + for p in projects: + if p.name == proj_name: + project = p + + if project: + log.debug(f"Project: {project.id}") + log.debug(f"project={project}") + + new_proj = Project(name="New project", active=True) + new_proj = jms_api.create_project(new_proj, replace=True) + # Delete project again + jms_api.delete_project(new_proj) + + log.debug("=== JobDefinitions ===") + project_api = ProjectApi(client, project.id) + job_definitions = project_api.get_job_definitions(active=True) + job_def = job_definitions[0] + log.debug(f"job_definition={job_def}") + + log.debug("=== Jobs ===") + all_jobs = project_api.get_jobs(fields="all") + log.debug(f"# jobs: {len(all_jobs)}") + if all_jobs: + log.debug(f"dp0={all_jobs[0]}") + + pending_jobs = project_api.get_jobs(eval_status="pending") + log.debug(f"Pending jobs: {[j.id for j in pending_jobs]}") + + # Alternative access with manually instantiated project + proj = jms_api.get_projects(name=proj_name)[0] + project_api = ProjectApi(client, proj.id) + evaluated_jobs = project_api.get_jobs(eval_status="evaluated", fields="all") + log.debug(f"Evaluated jobs: {[j.id for j in evaluated_jobs]}") + + # Access jobs data without objects + dp_data = project_api.get_jobs(limit=3, as_objects=False) + log.debug(f"dp_data={dp_data}") + + # Create some Jobs + new_jobs = [ + Job(name=f"new_dp_{i}", eval_status="pending", job_definition_id=job_def.id) + for i in range(10) + ] + created_jobs = project_api.create_jobs(new_jobs) + + # Delete Jobs again + project_api.delete_jobs(created_jobs) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/jms/test_job_definitions.py b/tests/jms/test_job_definitions.py index 5b41bfea2..f5a8acee2 100644 --- a/tests/jms/test_job_definitions.py +++ b/tests/jms/test_job_definitions.py @@ -1,5 +1,6 @@ import logging +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import JobDefinition, Project from tests.rep_test import REPTestCase @@ -8,19 +9,21 @@ class JobDefinitionsTest(REPTestCase): def test_job_definition_delete(self): - client = self.jms_client() + client = self.client() proj_name = f"rep_client_test_jms_JobDefinitionTest_{self.run_id}" proj = Project(name=proj_name, active=True) - proj = client.create_project(proj, replace=True) + jms_api = JmsApi(client) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) job_def = JobDefinition(name="New Config", active=True) - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] - assert len(proj.get_job_definitions()) == 1 + assert len(project_api.get_job_definitions()) == 1 - proj.delete_job_definitions([JobDefinition(id=job_def.id)]) + project_api.delete_job_definitions([JobDefinition(id=job_def.id)]) - assert len(proj.get_job_definitions()) == 0 + assert len(project_api.get_job_definitions()) == 0 - client.delete_project(proj) + jms_api.delete_project(proj) diff --git a/tests/jms/test_jobs.py b/tests/jms/test_jobs.py index ab799c4c8..e81afb8f3 100644 --- a/tests/jms/test_jobs.py +++ b/tests/jms/test_jobs.py @@ -13,6 +13,7 @@ from examples.mapdl_motorbike_frame.project_setup import create_project from marshmallow.utils import missing +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import Job, JobDefinition, Project from ansys.rep.client.jms.schema.job import JobSchema from tests.rep_test import REPTestCase @@ -127,31 +128,33 @@ def test_job_serialization(self): def test_job_integration(self): - client = self.jms_client() + client = self.client() proj_name = f"dcs_client_test_jobs_JobTest_{self.run_id}" proj = Project(name=proj_name, active=True) - proj = client.create_project(proj, replace=True) + jms_api = JmsApi(client) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) job_def = JobDefinition(name="New Config", active=True) - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] # test creating, update and delete with no jobs - jobs = proj.create_jobs([]) + jobs = project_api.create_jobs([]) self.assertEqual(len(jobs), 0) - jobs = proj.create_jobs([], as_objects=True) + jobs = project_api.create_jobs([], as_objects=True) self.assertEqual(len(jobs), 0) - jobs = proj.update_jobs([]) + jobs = project_api.update_jobs([]) self.assertEqual(len(jobs), 0) - jobs = proj.update_jobs([], as_objects=True) + jobs = project_api.update_jobs([], as_objects=True) self.assertEqual(len(jobs), 0) - proj.delete_jobs([]) + project_api.delete_jobs([]) jobs = [ Job(name=f"dp_{i}", eval_status="inactive", job_definition_id=job_def.id) for i in range(10) ] - jobs = proj.create_jobs(jobs) + jobs = project_api.create_jobs(jobs) for job in jobs: # check that all fields are populated (i.e. request params include fields="all") self.assertEqual(job.creator, None) @@ -159,7 +162,7 @@ def test_job_integration(self): self.assertEqual(job.fitness, None) self.assertTrue(job.executed_task_definition_level is not None) - jobs = proj.get_jobs() + jobs = project_api.get_jobs() for job in jobs: # check that all fields are populated (i.e. request params include fields="all") self.assertEqual(job.creator, None) @@ -167,10 +170,10 @@ def test_job_integration(self): self.assertEqual(job.fitness, None) self.assertTrue(job.executed_task_definition_level is not None) # fill some of them - job.creator = "dcs-client" - job.note = f"test dp{job.id} update" + job.creator = "rep-client" + job.note = f"test job{job.id} update" - jobs = proj.update_jobs(jobs) + jobs = project_api.update_jobs(jobs) for job in jobs: # check that all fields are populated (i.e. request params include fields="all") self.assertTrue(job.creator is not None) @@ -179,66 +182,76 @@ def test_job_integration(self): self.assertEqual(job.fitness, None) self.assertTrue(job.executed_task_definition_level is not None) - jobs = proj.get_jobs(limit=2, fields=["id", "creator", "note"]) + jobs = project_api.get_jobs(limit=2, fields=["id", "creator", "note"]) self.assertEqual(len(jobs), 2) for job in jobs: - self.assertEqual(job.creator, "dcs-client") - self.assertEqual(job.note, f"test dp{job.id} update") + self.assertEqual(job.creator, "rep-client") + self.assertEqual(job.note, f"test job{job.id} update") self.assertEqual(job.job_definition_id, missing) - proj.delete_jobs([Job(id=job.id) for job in jobs]) - jobs = proj.get_jobs() + project_api.delete_jobs([Job(id=job.id) for job in jobs]) + jobs = project_api.get_jobs() self.assertEqual(len(jobs), 8) + new_jobs = project_api.copy_jobs([Job(id=job.id) for job in jobs[:3]]) + for i in range(3): + self.assertEqual(new_jobs[i].creator, jobs[i].creator) + self.assertEqual(new_jobs[i].note, jobs[i].note) + self.assertEqual(new_jobs[i].job_definition_id, jobs[i].job_definition_id) + all_jobs = project_api.get_jobs() + self.assertEqual(len(all_jobs), len(jobs) + len(new_jobs)) + # Delete project - client.delete_project(proj) + jms_api.delete_project(proj) def test_job_update(self): - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"test_job_update_{uuid.uuid4().hex[:8]}" project = create_project(client=client, name=proj_name, num_jobs=2) project.active = False - project = client.update_project(project) + project = jms_api.update_project(project) + project_api = ProjectApi(client, project.id) - job_def = project.get_job_definitions()[0] - params = project.get_parameter_definitions(id=job_def.parameter_definition_ids) + job_def = project_api.get_job_definitions()[0] + params = project_api.get_parameter_definitions(id=job_def.parameter_definition_ids) input_parameters = [p.name for p in params if p.mode == "input"] - jobs = project.get_jobs(fields="all") + jobs = project_api.get_jobs(fields="all") ref_values = {job.id: copy.deepcopy(job.values) for job in jobs} # change eval status with full DP object for job in jobs: job.eval_status = "pending" - jobs = project.update_jobs(jobs) + jobs = project_api.update_jobs(jobs) for job in jobs: for p_name in input_parameters: self.assertEqual(job.values[p_name], ref_values[job.id][p_name]) # change eval status with minimal DP object - jobs = project.get_jobs(fields=["id", "eval_status"]) + jobs = project_api.get_jobs(fields=["id", "eval_status"]) for job in jobs: job.eval_status = "pending" - jobs = project.update_jobs(jobs) + jobs = project_api.update_jobs(jobs) for job in jobs: for p_name in input_parameters: self.assertEqual(job.values[p_name], ref_values[job.id][p_name]) # change eval status with partial DP object including config id - jobs = project.get_jobs(fields=["id", "job_definition_id", "eval_status"]) + jobs = project_api.get_jobs(fields=["id", "job_definition_id", "eval_status"]) for job in jobs: job.eval_status = "pending" - jobs = project.update_jobs(jobs) + jobs = project_api.update_jobs(jobs) for job in jobs: for p_name in input_parameters: self.assertEqual(job.values[p_name], ref_values[job.id][p_name], p_name) - client.delete_project(project) + jms_api.delete_project(project) if __name__ == "__main__": diff --git a/tests/jms/test_parameter_definitions.py b/tests/jms/test_parameter_definitions.py index 276140bd0..332077392 100644 --- a/tests/jms/test_parameter_definitions.py +++ b/tests/jms/test_parameter_definitions.py @@ -10,6 +10,7 @@ from marshmallow.utils import missing +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import JobDefinition, Project from ansys.rep.client.jms.resource.parameter_definition import ( BoolParameterDefinition, @@ -163,28 +164,30 @@ def test_parameter_definition_serialization(self): def test_parameter_definition_integration(self): - client = self.jms_client() + client = self.client() proj_name = f"test_jms_ParameterDefinitionTest_{self.run_id}" proj = Project(name=proj_name, active=True) - proj = client.create_project(proj, replace=True) + jms_api = JmsApi(client) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) ip = IntParameterDefinition(name="int_param", upper_limit=27) sp = StringParameterDefinition(name="s_param", value_list=["l1", "l2"]) - ip = proj.create_parameter_definitions([ip])[0] - sp = proj.create_parameter_definitions([sp])[0] + ip = project_api.create_parameter_definitions([ip])[0] + sp = project_api.create_parameter_definitions([sp])[0] job_def = JobDefinition(name="New Config", active=True) job_def.parameter_definition_ids = [ip.id, sp.id] - job_def = proj.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] self.assertEqual(len(job_def.parameter_definition_ids), 2) fp = FloatParameterDefinition(name="f_param", display_name="A Float Parameter") bp = BoolParameterDefinition(name="b_param", display_name="A Bool Parameter", default=False) - fp = proj.create_parameter_definitions([fp])[0] - bp = proj.create_parameter_definitions([bp])[0] + fp = project_api.create_parameter_definitions([fp])[0] + bp = project_api.create_parameter_definitions([bp])[0] job_def.parameter_definition_ids.extend([p.id for p in [bp, fp]]) - job_def = proj.update_job_definitions([job_def])[0] + job_def = project_api.update_job_definitions([job_def])[0] self.assertEqual(len(job_def.parameter_definition_ids), 4) self.assertTrue(fp.id in job_def.parameter_definition_ids) self.assertTrue(bp.id in job_def.parameter_definition_ids) @@ -198,7 +201,7 @@ def test_parameter_definition_integration(self): # self.assertEqual(job_def.parameter_definitions[2].lower_limit, 4.5) # Delete project - client.delete_project(proj) + jms_api.delete_project(proj) if __name__ == "__main__": diff --git a/tests/jms/test_project_permissions.py b/tests/jms/test_project_permissions.py index ce42c185a..3d0ac479a 100644 --- a/tests/jms/test_project_permissions.py +++ b/tests/jms/test_project_permissions.py @@ -10,63 +10,65 @@ import unittest import uuid -from ansys.rep.client.auth import Client as AuthClient -from ansys.rep.client.auth import User +from ansys.rep.client import Client +from ansys.rep.client.auth import AuthApi, User from ansys.rep.client.exceptions import ClientError -from ansys.rep.client.jms import Client +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import JobDefinition, Project, ProjectPermission from tests.rep_test import REPTestCase log = logging.getLogger(__name__) -def add_job_definition_to_project(project, config_name): - log.info(f"=== Add job_definition {config_name} to project {project.id}") +def add_job_definition_to_project(project_api: ProjectApi, config_name): + log.info(f"=== Add job_definition {config_name} to project {project_api.project_id}") job_def = JobDefinition(name=config_name, active=True) - project.create_job_definitions([job_def]) + project_api.create_job_definitions([job_def]) -def grant_permissions(proj, user): +def grant_permissions(project_api: ProjectApi, user): log.info(f"=== Granting permissions to {user.username}") - permissions = proj.get_permissions() + permissions = project_api.get_permissions() log.info(f"Permissions before: {permissions}") permissions.append( ProjectPermission( permission_type="user", value_name=user.username, role="writer", value_id=user.id ) ) - proj.update_permissions(permissions) - permissions = proj.get_permissions() + project_api.update_permissions(permissions) + permissions = project_api.get_permissions() log.info(f"Permissions after: {permissions}") -def remove_permissions(proj, user): +def remove_permissions(project_api: ProjectApi, user): log.info(f"=== Removing permissions to {user.username}") - permissions = proj.get_permissions() + permissions = project_api.get_permissions() log.info(f"Permissions before: {permissions}") permissions = [p for p in permissions if p.value_name != user.username] - proj.update_permissions(permissions) - permissions = proj.get_permissions() + project_api.update_permissions(permissions) + permissions = project_api.get_permissions() log.info(f"Permissions after: {permissions}") class ProjectPermissionsTest(REPTestCase): def test_get_project_permissions(self): - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"test_dps_get_permissions_test_{self.run_id}" proj = Project(name=proj_name, active=True, priority=10) - proj = client.create_project(proj, replace=True) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) - perms = proj.get_permissions() + perms = project_api.get_permissions() self.assertEqual(len(perms), 1) self.assertEqual(perms[0].value_name, self.username) self.assertEqual(perms[0].role, "admin") self.assertEqual(perms[0].permission_type, "user") # Delete project - client.delete_project(proj) + jms_api.delete_project(proj) def test_modify_project_permissions(self): user_credentials = { @@ -75,11 +77,12 @@ def test_modify_project_permissions(self): } proj_name = f"test_dps_get_permissions_test_{uuid.uuid4().hex[:8]}" - auth_client = AuthClient(self.rep_url, username=self.username, password=self.password) - existing_users = [u.username for u in auth_client.get_users()] + client = self.client() + auth_api = AuthApi(client) + existing_users = [u.username for u in auth_api.get_users()] if user_credentials["user1"]["username"] not in existing_users: - user1 = auth_client.create_user( + user1 = auth_api.create_user( User( username=user_credentials["user1"]["username"], password=user_credentials["user1"]["password"], @@ -89,14 +92,14 @@ def test_modify_project_permissions(self): else: user1 = [ u - for u in auth_client.get_users() + for u in auth_api.get_users() if u.username == user_credentials["user1"]["username"] ][0] log.info(f"User 1: {user1}") if user_credentials["user2"]["username"] not in existing_users: - user2 = auth_client.create_user( + user2 = auth_api.create_user( User( username=user_credentials["user2"]["username"], password=user_credentials["user2"]["password"], @@ -106,13 +109,13 @@ def test_modify_project_permissions(self): else: user2 = [ u - for u in auth_client.get_users() + for u in auth_api.get_users() if u.username == user_credentials["user2"]["username"] ][0] log.info(f"User 2: {user2}") - # user1 creates a project and a config + # user1 creates a project and a job definition client1 = Client( rep_url=self.rep_url, username=user1.username, @@ -120,36 +123,40 @@ def test_modify_project_permissions(self): ) log.info(f"Client connected at {client1.rep_url} with user {user1.username}") + root_api1 = JmsApi(client1) proj = Project(name=proj_name, priority=1, active=True) - proj = client1.create_project(proj, replace=True) + proj = root_api1.create_project(proj, replace=True) + project_api = ProjectApi(client1, proj.id) log.info(f"Created new project with id={proj.id}") - add_job_definition_to_project(proj, f"Config 1 - {user1.username}") - self.assertEqual(len(proj.get_job_definitions()), 1) + add_job_definition_to_project(project_api, f"Config 1 - {user1.username}") + self.assertEqual(len(project_api.get_job_definitions()), 1) # user1 shares the project with user2 - grant_permissions(proj, user2) - permissions = proj.get_permissions() + grant_permissions(project_api, user2) + permissions = project_api.get_permissions() self.assertEqual(len(permissions), 2) self.assertIn(user1.username, [x.value_name for x in permissions]) self.assertIn(user2.username, [x.value_name for x in permissions]) - # user1 appends a config to the project + # user1 appends a job definition to the project client2 = Client( rep_url=self.rep_url, username=user2.username, password=user_credentials["user2"]["password"], ) + root_api2 = JmsApi(client2) log.info(f"Client connected at {client2.rep_url} with user {user2.username}") - proj_user2 = client2.get_project(name=proj_name) - add_job_definition_to_project(proj_user2, f"Config 2 - {user2.username}") + proj_user2 = root_api2.get_project(name=proj_name) + project_api2 = ProjectApi(client2, proj_user2.id) + add_job_definition_to_project(project_api2, f"Config 2 - {user2.username}") - self.assertEqual(len(proj_user2.get_job_definitions()), 2) + self.assertEqual(len(project_api2.get_job_definitions()), 2) # user1 removes permissions to user2 - remove_permissions(proj, user2) - permissions = proj.get_permissions() + remove_permissions(project_api, user2) + permissions = project_api.get_permissions() self.assertEqual(len(permissions), 1) self.assertIn(user1.username, [x.value_name for x in permissions]) self.assertNotIn(user2.username, [x.value_name for x in permissions]) @@ -160,13 +167,13 @@ def test_modify_project_permissions(self): username=user2.username, password=user_credentials["user2"]["password"], ) - + root_api2 = JmsApi(client2) time.sleep(5) # this should return 403 forbidden by now, instead it returns an empty response except_obj = None try: - proj_user2 = client2.get_project(id=proj_user2.id) + proj_user2 = root_api2.get_project(id=proj_user2.id) except ClientError as e: except_obj = e log.error(str(e)) @@ -174,7 +181,7 @@ def test_modify_project_permissions(self): self.assertIsNotNone(except_obj) self.assertTrue(except_obj.response.status_code, 403) - client1.delete_project(proj) + root_api1.delete_project(proj) if __name__ == "__main__": diff --git a/tests/jms/test_projects.py b/tests/jms/test_projects.py index ca1a9ad74..f2dd02212 100644 --- a/tests/jms/test_projects.py +++ b/tests/jms/test_projects.py @@ -11,9 +11,10 @@ import time import unittest -from examples.mapdl_motorbike_frame.project_setup import create_project +from examples.mapdl_motorbike_frame.project_setup import create_project as motorbike_create_project from marshmallow.utils import missing +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import JobDefinition, LicenseContext, Project from ansys.rep.client.jms.schema.project import ProjectSchema from tests.rep_test import REPTestCase @@ -107,82 +108,87 @@ def test_project_serialization(self): def test_project_integration(self): - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"test_dps_ProjectTest_{self.run_id}" proj = Project(name=proj_name, active=True, priority=10) - proj = client.create_project(proj, replace=True) + proj = jms_api.create_project(proj, replace=True) - proj = client.get_project(id=proj.id) + proj = jms_api.get_project(id=proj.id) self.assertTrue(proj.creation_time is not None) self.assertEqual(proj.priority, 10) self.assertEqual(proj.active, True) - proj = client.get_projects(name=proj.name, statistics=True)[0] + proj = jms_api.get_projects(name=proj.name, statistics=True)[0] self.assertEqual(proj.statistics["num_jobs"], 0) # statistics["eval_status"] might get few seconds until is populated on the server timeout = time.time() + 120 while not proj.statistics["eval_status"] and time.time() < timeout: time.sleep(5) - proj = client.get_projects(id=proj.id, statistics=True)[0] + proj = jms_api.get_projects(id=proj.id, statistics=True)[0] self.assertEqual(proj.statistics["eval_status"]["prolog"], 0) self.assertEqual(proj.statistics["eval_status"]["failed"], 0) - proj = client.get_project(id=proj.id) + proj = jms_api.get_project(id=proj.id) proj.active = False - proj = client.update_project(proj) + proj = jms_api.update_project(proj) self.assertEqual(proj.active, False) # Delete project - client.delete_project(proj) + jms_api.delete_project(proj) def test_project_copy(self): - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"test_dps_ProjectCopyTest_{self.run_id}" proj = Project(name=proj_name, active=True, priority=10) - proj = client.create_project(proj, replace=True) + proj = jms_api.create_project(proj, replace=True) tgt_name = proj_name + "_copy1" - proj1_id = client.copy_project(proj, tgt_name) - copied_proj1 = client.get_project(name=tgt_name) + project_api = ProjectApi(client, proj.id) + proj1_id = project_api.copy_project(tgt_name) + copied_proj1 = jms_api.get_project(name=tgt_name) self.assertIsNotNone(copied_proj1) tgt_name = proj_name + "_copy2" - proj.copy(tgt_name) - copied_proj2 = client.get_project(name=tgt_name) + project_api.copy_project(tgt_name) + copied_proj2 = jms_api.get_project(name=tgt_name) self.assertIsNotNone(copied_proj2) # Delete projects - client.delete_project(copied_proj1) - client.delete_project(copied_proj2) - client.delete_project(proj) + jms_api.delete_project(copied_proj1) + jms_api.delete_project(copied_proj2) + jms_api.delete_project(proj) @unittest.expectedFailure def test_project_license_context(self): - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"test_dps_ProjectTest_license_context_{self.run_id}" proj = Project(id=proj_name, active=True, priority=10) - proj = client.create_project(proj, replace=True) + proj = jms_api.create_project(proj, replace=True) + project_api = ProjectApi(client, proj.id) # Create new license context in DPS - license_contexts = proj.create_license_contexts() + license_contexts = project_api.create_license_contexts() self.assertEqual(len(license_contexts), 1) self.assertGreater(len(license_contexts[0].context_id), 0) self.assertEqual(len(license_contexts[0].environment), 2) - license_contexts = proj.get_license_contexts() + license_contexts = project_api.get_license_contexts() self.assertEqual(len(license_contexts), 1) self.assertGreater(len(license_contexts[0].context_id), 0) self.assertEqual(len(license_contexts[0].environment), 2) # Terminate license context - proj.delete_license_contexts() - license_contexts = proj.get_license_contexts() + project_api.delete_license_contexts() + license_contexts = project_api.get_license_contexts() self.assertEqual(len(license_contexts), 0) # Set a license context from outside= @@ -192,7 +198,7 @@ def test_project_license_context(self): "ANSYS_HPC_PARAMETRIC_SERVER": "my_server", } ) - license_contexts = proj.update_license_contexts([lc]) + license_contexts = project_api.update_license_contexts([lc]) self.assertEqual(len(license_contexts), 1) self.assertEqual(license_contexts[0].context_id, "my_id") self.assertEqual(len(license_contexts[0].environment), 2) @@ -201,7 +207,7 @@ def test_project_license_context(self): license_contexts[0].environment["ANSYS_HPC_PARAMETRIC_SERVER"], "my_server" ) - license_contexts = proj.get_license_contexts() + license_contexts = project_api.get_license_contexts() self.assertEqual(len(license_contexts), 1) self.assertEqual(license_contexts[0].context_id, "my_id") self.assertEqual(len(license_contexts[0].environment), 2) @@ -211,63 +217,69 @@ def test_project_license_context(self): ) # Remove the license context set from outside again - proj.delete_license_contexts() - license_contexts = proj.get_license_contexts() + project_api.delete_license_contexts() + license_contexts = project_api.get_license_contexts() self.assertEqual(len(license_contexts), 0) # Delete project - client.delete_project(proj) + jms_api.delete_project(proj) def test_project_delete_job_definition(self): - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"test_dps_ProjectTest_delete_config_{self.run_id}" proj = Project(name=proj_name, active=True, priority=10) - proj = client.create_project(proj, replace=True) + proj = jms_api.create_project(proj, replace=True) + + project_api = ProjectApi(client, proj.id) job_def = JobDefinition(name="Config1") - job_def = proj.create_job_definitions([job_def])[0] - self.assertEqual(len(proj.get_job_definitions()), 1) - proj.delete_job_definitions([job_def]) - self.assertEqual(len(proj.get_job_definitions()), 0) + job_def = project_api.create_job_definitions([job_def])[0] + self.assertEqual(len(project_api.get_job_definitions()), 1) + project_api.delete_job_definitions([job_def]) + self.assertEqual(len(project_api.get_job_definitions()), 0) - client.delete_project(proj) + jms_api.delete_project(proj) def test_project_archive_restore(self): num_jobs = 2 - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"test_dps_project_archive_restore_{self.run_id}" # Setup project to work with - project = create_project(client=client, name=proj_name, num_jobs=num_jobs) + project = motorbike_create_project(client=client, name=proj_name, num_jobs=num_jobs) project.active = False project.priority = 6 - project = client.update_project(project) + project = jms_api.update_project(project) - restored_proj_name = f"{proj_name}-restored" restored_project = None + project_api = ProjectApi(client, project.id) with tempfile.TemporaryDirectory() as tpath: # Archive project - archive_path = client.archive_project(project, tpath, include_job_files=True) + archive_path = project_api.archive_project(tpath, include_job_files=True) self.assertTrue(os.path.exists(archive_path)) log.info(f"Archive size {os.path.getsize(archive_path)} bytes") self.assertGreater(os.path.getsize(archive_path), 2e3) # file larger than 2 KB size # Restore project - restored_project = client.restore_project(archive_path, restored_proj_name) + restored_project = jms_api.restore_project(archive_path) + restored_project_api = ProjectApi(client, restored_project.id) self.assertEqual(restored_project.active, False) self.assertEqual(restored_project.priority, 6) self.assertEqual( - len(project.get_job_definitions()), len(restored_project.get_job_definitions()) + len(project_api.get_job_definitions()), + len(restored_project_api.get_job_definitions()), ) - self.assertEqual(len(project.get_jobs()), len(restored_project.get_jobs())) + self.assertEqual(len(project_api.get_jobs()), len(restored_project_api.get_jobs())) - client.delete_project(project) - client.delete_project(restored_project) + jms_api.delete_project(project) + jms_api.delete_project(restored_project) if __name__ == "__main__": diff --git a/tests/jms/test_task_definition_templates.py b/tests/jms/test_task_definition_templates.py index 278281318..f6fb264a2 100644 --- a/tests/jms/test_task_definition_templates.py +++ b/tests/jms/test_task_definition_templates.py @@ -12,6 +12,7 @@ from marshmallow.utils import missing +from ansys.rep.client.jms import JmsApi from ansys.rep.client.jms.resource.task_definition_template import TaskDefinitionTemplate from ansys.rep.client.jms.schema.task_definition_template import TaskDefinitionTemplateSchema from tests.rep_test import REPTestCase @@ -76,18 +77,19 @@ def test_template_deserialization(self): def test_template_integration(self): - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) # Test get queries - templates = client.get_task_definition_templates() + templates = jms_api.get_task_definition_templates() self.assertGreater(len(templates), 0) self.assertTrue(templates[0].id is not None) - templates = client.get_task_definition_templates(as_objects=False) + templates = jms_api.get_task_definition_templates(as_objects=False) self.assertGreater(len(templates), 0) self.assertTrue(templates[0]["id"] is not None) - templates = client.get_task_definition_templates(as_objects=False, fields=["name", "data"]) + templates = jms_api.get_task_definition_templates(as_objects=False, fields=["name", "data"]) self.assertGreater(len(templates), 0) log.info(f"templates={json.dumps(templates, indent=4)}") if templates: @@ -95,33 +97,33 @@ def test_template_integration(self): self.assertTrue("name" in templates[0]["data"]["software_requirements"][0].keys()) self.assertTrue("version" in templates[0]["data"]["software_requirements"][0].keys()) - templates = client.get_task_definition_templates(fields=["name"]) + templates = jms_api.get_task_definition_templates(fields=["name"]) if templates: self.assertTrue(templates[0].data == missing) # Copy template template_name = f"copied_template_{self.run_id}" - templates = client.get_task_definition_templates(limit=1) + templates = jms_api.get_task_definition_templates(limit=1) self.assertEqual(len(templates), 1) template = TaskDefinitionTemplate(name=template_name, data=templates[0].data) - templates = client.create_task_definition_templates([template]) + templates = jms_api.create_task_definition_templates([template]) self.assertEqual(len(templates), 1) template = templates[0] self.assertEqual(template.name, template_name) # Modify copied template template.data["software_requirements"][0]["version"] = "2.0.1" - templates = client.update_task_definition_templates([template]) + templates = jms_api.update_task_definition_templates([template]) self.assertEqual(len(templates), 1) template = templates[0] self.assertEqual(template.data["software_requirements"][0]["version"], "2.0.1") self.assertEqual(template.name, template_name) # Delete copied template - client.delete_task_definition_templates([template]) + jms_api.delete_task_definition_templates([template]) - templates = client.get_task_definition_templates(name=template_name) + templates = jms_api.get_task_definition_templates(name=template_name) self.assertEqual(len(templates), 0) diff --git a/tests/jms/test_task_files.py b/tests/jms/test_task_files.py index b2f3be17e..8a8e41aa9 100644 --- a/tests/jms/test_task_files.py +++ b/tests/jms/test_task_files.py @@ -13,6 +13,7 @@ from examples.mapdl_motorbike_frame.project_setup import create_project +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import File from tests.rep_test import REPTestCase @@ -22,13 +23,16 @@ class TaskFilesTest(REPTestCase): def test_task_files_in_single_task_definition_project(self): num_jobs = 5 - client = self.jms_client() + client = self.client() proj_name = f"test_jobs_TaskFilesTest_{self.run_id}" # Setup MAPDL motorbike frame project to work with proj = create_project(client=client, name=proj_name, num_jobs=num_jobs) proj.active = False - proj = client.update_project(proj) + jms_api = JmsApi(client) + proj = jms_api.update_project(proj) + + project_api = ProjectApi(client, proj.id) cwd = os.path.dirname(__file__) ex_dir = os.path.join(cwd, "..", "..", "examples", "mapdl_motorbike_frame") @@ -36,7 +40,7 @@ def test_task_files_in_single_task_definition_project(self): # Create a modified MAPDL input file that reads an extra task file # and writes out an extra result file - mac_file = proj.get_files(limit=1, content=True)[0] + mac_file = project_api.get_files(limit=1, content=True)[0] content = mac_file.content.decode("utf-8") lines = content.splitlines() for i, l in enumerate(lines): @@ -56,7 +60,7 @@ def test_task_files_in_single_task_definition_project(self): f.write("\n".join(lines)) # Add specific task files to tasks - tasks = proj.get_tasks() + tasks = project_api.get_tasks() for i, t in enumerate(tasks): log.info(f"Modify task {t.id}") files = [] @@ -94,7 +98,7 @@ def test_task_files_in_single_task_definition_project(self): ) ) - files = proj.create_files(files) + files = project_api.create_files(files) file_ids = {f.name: f.id for f in files} log.info(f"Add files: {file_ids}") @@ -102,7 +106,7 @@ def test_task_files_in_single_task_definition_project(self): t.output_file_ids.append(file_ids["task_result"]) # Not yet supported: t.eval_status = 'pending' - tasks1 = proj.update_tasks(tasks) + tasks1 = project_api.update_tasks(tasks) self.assertEqual(len(tasks1), 5) # Check num files existing before evaluation @@ -114,17 +118,17 @@ def test_task_files_in_single_task_definition_project(self): # Wait for the evaluation of all Jobs proj.active = True - client.update_project(proj) + jms_api.update_project(proj) - def wait_for_evaluation_of_all_jobs(proj): - job_def = proj.get_job_definitions()[0] + def wait_for_evaluation_of_all_jobs(project_api): + job_def = project_api.get_job_definitions()[0] num_jobs_finished = 0 t1 = datetime.datetime.now() dt = 0.0 - task_def = proj.get_task_definitions(job_def.task_definition_ids[0])[0] + task_def = project_api.get_task_definitions(job_def.task_definition_ids[0])[0] max_eval_time = task_def.max_execution_time * 4 while num_jobs_finished < num_jobs and dt < max_eval_time: - jobs = proj.get_jobs(fields=["id", "eval_status"]) + jobs = project_api.get_jobs(fields=["id", "eval_status"]) num_jobs_finished = len( [job for job in jobs if job.eval_status in ["evaluated", "timeout", "failed"]] ) @@ -134,14 +138,14 @@ def wait_for_evaluation_of_all_jobs(proj): time.sleep(5) dt = (datetime.datetime.now() - t1).total_seconds() - wait_for_evaluation_of_all_jobs(proj) + wait_for_evaluation_of_all_jobs(project_api) # Check evaluated design points, tasks and files - jobs = proj.get_jobs() + jobs = project_api.get_jobs() for job in jobs: self.assertEqual(job.eval_status, "evaluated") - def check_evaluated_tasks(proj, tasks): + def check_evaluated_tasks(project_api, tasks): for t in tasks: log.info(f"=== Task ===") log.info(f"id={t.id} eval_status={t.eval_status}") @@ -158,14 +162,14 @@ def check_evaluated_tasks(proj, tasks): # All input files are owned by task self.assertEqual(len(set(t.input_file_ids).intersection(t.owned_file_ids)), 2) - input_files = proj.get_files(id=t.input_file_ids) + input_files = project_api.get_files(id=t.input_file_ids) self.assertEqual( set([f.name for f in input_files]), set(["inp", "inp2"]) ) # Check input file names owned_output_file_ids = set(t.output_file_ids).intersection(t.owned_file_ids) self.assertEqual(len(owned_output_file_ids), 1) # 1 output file is owned - owned_output_files = proj.get_files(id=owned_output_file_ids) + owned_output_files = project_api.get_files(id=owned_output_file_ids) self.assertEqual(owned_output_files[0].name, "task_result") inherited_output_file_ids = set(t.output_file_ids).intersection( @@ -173,7 +177,7 @@ def check_evaluated_tasks(proj, tasks): ) # 5 output files are inherited self.assertEqual(len(inherited_output_file_ids), 5) - inherited_output_files = proj.get_files(id=inherited_output_file_ids) + inherited_output_files = project_api.get_files(id=inherited_output_file_ids) self.assertEqual( set([f.name for f in inherited_output_files]), set(["out", "img", "err", "console_output"]), @@ -182,23 +186,23 @@ def check_evaluated_tasks(proj, tasks): # Find the task output file and compare the contained variable task_var with task id intersection = set(t.output_file_ids).intersection(t.owned_file_ids) fid = intersection.pop() - file = proj.get_files(id=fid, content=True)[0] + file = project_api.get_files(id=fid, content=True)[0] content = file.content.decode("utf-8") log.info(f"Task output file id={fid} content={content} task_id={t.id}") # Task id must show up in task output file self.assertTrue(t.id in content) - tasks2 = proj.get_tasks() - check_evaluated_tasks(proj, tasks2) + tasks2 = project_api.get_tasks() + check_evaluated_tasks(project_api, tasks2) # Set project to inactive, reset design points and check what we have proj.active = False - proj = client.update_project(proj) - jobs = proj.get_jobs(fields=["id", "eval_status"]) + proj = jms_api.update_project(proj) + jobs = project_api.get_jobs(fields=["id", "eval_status"]) for job in jobs: job.eval_status = "pending" - proj.update_jobs(jobs) - tasks3 = proj.get_tasks() + project_api.update_jobs(jobs) + tasks3 = project_api.get_tasks() # Compare tasks with the ones created at the begin log.info(f"Tasks 1: {[t.id for t in tasks1]}") log.info(f"Tasks 3: {[t.id for t in tasks3]}") @@ -214,15 +218,15 @@ def check_evaluated_tasks(proj, tasks): # Wait again for the evaluation of all Jobs proj.active = True - proj = client.update_project(proj) - wait_for_evaluation_of_all_jobs(proj) + proj = jms_api.update_project(proj) + wait_for_evaluation_of_all_jobs(project_api) # Check evaluated tasks - tasks4 = proj.get_tasks() - check_evaluated_tasks(proj, tasks4) + tasks4 = project_api.get_tasks() + check_evaluated_tasks(project_api, tasks4) # Cleanup - client.delete_project(proj) + jms_api.delete_project(proj) # TODO # def test_task_files_in_multi_task_definition_project(self): diff --git a/tests/jms/test_tasks.py b/tests/jms/test_tasks.py index 69254a36e..6fcf6498d 100644 --- a/tests/jms/test_tasks.py +++ b/tests/jms/test_tasks.py @@ -13,6 +13,7 @@ from examples.mapdl_motorbike_frame.project_setup import create_project +from ansys.rep.client.jms import JmsApi, ProjectApi from ansys.rep.client.jms.resource import Job, JobDefinition, Project, TaskDefinition from ansys.rep.client.jms.schema.task import TaskSchema from tests.rep_test import REPTestCase @@ -99,29 +100,33 @@ def test_task_integration(self): # In case, you can create such project running the script # examples/mapdl_motorbike_frame/project_setup.py - client = self.jms_client() + client = self.client() proj_name = "mapdl_motorbike_frame" - project = client.get_projects(name=proj_name, sort="-creation_time")[0] - tasks = project.get_tasks(limit=5) + jms_api = JmsApi(client) + project = jms_api.get_projects(name=proj_name, sort="-creation_time")[0] + project_api = ProjectApi(client, project.id) + tasks = project_api.get_tasks(limit=5) self.assertEqual(len(tasks), 5) - jobs = project.get_jobs(limit=5) + jobs = project_api.get_jobs(limit=5) for job in jobs: - tasks = job.get_tasks() + tasks = project_api.get_tasks(job_id=job.id) self.assertEqual(tasks[0].job_id, job.id) def test_job_sync(self): # create base project with 1 process step and 3 design points num_jobs = 3 - client = self.jms_client() + client = self.client() + jms_api = JmsApi(client) proj_name = f"test_desing_point_sync_{uuid.uuid4().hex[:8]}" project = Project( name=proj_name, active=False, priority=10, display_name="test_desing_point_sync" ) - project = client.create_project(project, replace=True) + project = jms_api.create_project(project, replace=True) + project_api = ProjectApi(client, project.id) task_def_1 = TaskDefinition( name="Task.1", @@ -132,19 +137,19 @@ def test_job_sync(self): execution_level=0, num_trials=1, ) - task_def_1 = project.create_task_definitions([task_def_1])[0] + task_def_1 = project_api.create_task_definitions([task_def_1])[0] job_def = JobDefinition( name="JobDefinition.1", active=True, task_definition_ids=[task_def_1.id] ) - job_def = project.create_job_definitions([job_def])[0] + job_def = project_api.create_job_definitions([job_def])[0] jobs = [] for i in range(num_jobs): - jobs.append(Job(name=f"Job.{i}", eval_status="pending")) - jobs = job_def.create_jobs(jobs) + jobs.append(Job(name=f"Job.{i}", eval_status="pending", job_definition_id=job_def.id)) + jobs = project_api.create_jobs(jobs) for job in jobs: - tasks = job.get_tasks() + tasks = project_api.get_tasks(job_id=job.id) self.assertEqual(len(tasks), 1) self.assertEqual(tasks[0].eval_status, "pending") @@ -158,33 +163,32 @@ def test_job_sync(self): execution_level=1, num_trials=1, ) - task_def_2 = project.create_task_definitions([task_def_2])[0] - job_def = project.get_job_definitions()[0] + task_def_2 = project_api.create_task_definitions([task_def_2])[0] + job_def = project_api.get_job_definitions()[0] job_def.task_definition_ids.append(task_def_2.id) - job_def = project.update_job_definitions([job_def])[0] + job_def = project_api.update_job_definitions([job_def])[0] # sync design points individually - jobs = project.get_jobs() - for job in jobs: - job._sync() + jobs = project_api.get_jobs() + project_api._sync_jobs(jobs) # verify that tasks were added and they're inactive for job in jobs: - tasks = job.get_tasks() + tasks = project_api.get_tasks(job_id=job.id) self.assertEqual(len(tasks), 2) self.assertEqual(tasks[0].eval_status, "pending") self.assertEqual(tasks[0].task_definition_snapshot.name, "Task.1") self.assertEqual(tasks[1].eval_status, "inactive") # set new tasks to pending - tasks = project.get_tasks(eval_status="inactive") + tasks = project_api.get_tasks(eval_status="inactive") for task in tasks: task.eval_status = "pending" - project.update_tasks(tasks) + project_api.update_tasks(tasks) # verify that tasks are pending and that task_definition_snapshots were created for job in jobs: - tasks = job.get_tasks() + tasks = project_api.get_tasks(job_id=job.id) self.assertEqual(len(tasks), 2) self.assertEqual(tasks[0].eval_status, "pending") self.assertEqual(tasks[0].task_definition_snapshot.name, "Task.1") @@ -201,31 +205,31 @@ def test_job_sync(self): execution_level=0, num_trials=1, ) - task_def_3 = project.create_task_definitions([task_def_3])[0] + task_def_3 = project_api.create_task_definitions([task_def_3])[0] job_def.task_definition_ids.append(task_def_3.id) - job_def = project.update_job_definitions([job_def])[0] + job_def = project_api.update_job_definitions([job_def])[0] # sync the first 2 design points in bulk - project._sync_jobs(jobs[:2]) + project_api._sync_jobs(jobs[:2]) # verify that tasks were added and they're inactive for job in jobs[:2]: - tasks = job.get_tasks() + tasks = project_api.get_tasks(job_id=job.id) self.assertEqual(len(tasks), 3) self.assertEqual(tasks[0].eval_status, "pending") self.assertEqual(tasks[1].eval_status, "pending") self.assertEqual(tasks[2].eval_status, "inactive") # verify that the third task wasn't added for DP2 - tasks = jobs[2].get_tasks() + tasks = project_api.get_tasks(job_id=jobs[2].id) self.assertEqual(len(tasks), 2) self.assertEqual(tasks[0].eval_status, "pending") self.assertEqual(tasks[1].eval_status, "pending") - client.delete_project(project) + jms_api.delete_project(project) - def wait_for_evaluation_of_job(self, project, job_id, max_eval_time): - job = project.get_jobs(id=job_id)[0] + def wait_for_evaluation_of_job(self, project_api, job_id, max_eval_time): + job = project_api.get_jobs(id=job_id)[0] t1 = datetime.datetime.now() dt = 0.0 while ( @@ -234,7 +238,7 @@ def wait_for_evaluation_of_job(self, project, job_id, max_eval_time): ): time.sleep(2) log.info(f" Waiting for job '{job.name}' to complete ... ") - job = project.get_jobs(id=job_id)[0] + job = project_api.get_jobs(id=job_id)[0] dt = (datetime.datetime.now() - t1).total_seconds() return job @@ -243,39 +247,40 @@ def test_sync_task_definition_snapshot(self): # verity that the process step snapshot of an evaluated task in not modified # on job:sync - client = self.jms_client() + client = self.client() proj_name = f"test_sync_task_definition_snapshot_{uuid.uuid4().hex[:8]}" project = create_project(client=client, name=proj_name, num_jobs=1) + project_api = ProjectApi(client, project.id) - job = project.get_jobs()[0] - tasks = job.get_tasks() + job = project_api.get_jobs()[0] + tasks = project_api.get_tasks(job_id=job.id) self.assertEqual(len(tasks), 1) self.assertEqual( tasks[0].task_definition_snapshot.software_requirements[0].name, "ANSYS Mechanical APDL" ) - job_def = project.get_job_definitions(id=job.job_definition_id)[0] - task_def = project.get_task_definitions(id=job_def.task_definition_ids[0])[0] + job_def = project_api.get_job_definitions(id=job.job_definition_id)[0] + task_def = project_api.get_task_definitions(id=job_def.task_definition_ids[0])[0] max_eval_time = task_def.max_execution_time * 4 - job = self.wait_for_evaluation_of_job(project, job.id, max_eval_time) + job = self.wait_for_evaluation_of_job(project_api, job.id, max_eval_time) self.assertEqual(job.eval_status, "evaluated") # modify application of the process step - task_def = project.get_task_definitions(id=job_def.task_definition_ids[0])[0] + task_def = project_api.get_task_definitions(id=job_def.task_definition_ids[0])[0] task_def.software_requirements[0].name = "NonExistingApp" - task_def = project.update_task_definitions([task_def])[0] + task_def = project_api.update_task_definitions([task_def])[0] # the application in the task.task_definition_snapshot should not be modified - job._sync() - tasks = job.get_tasks() + project_api._sync_jobs([job]) + tasks = project_api.get_tasks(job_id=job.id) self.assertEqual(len(tasks), 1) self.assertEqual(tasks[0].eval_status, "evaluated") self.assertEqual( tasks[0].task_definition_snapshot.software_requirements[0].name, "ANSYS Mechanical APDL" ) - client.delete_project(project) + JmsApi(client).delete_project(project) if __name__ == "__main__": diff --git a/tests/rep_test.py b/tests/rep_test.py index 0aebb09f5..0c03a0bca 100644 --- a/tests/rep_test.py +++ b/tests/rep_test.py @@ -10,7 +10,7 @@ import os import unittest -from ansys.rep.client.jms import Client +from ansys.rep.client import Client class REPTestCase(unittest.TestCase): @@ -48,5 +48,5 @@ def tearDown(self): # self.logger.removeHandler(self._stream_handler) pass - def jms_client(self): + def client(self): return Client(self.rep_url, self.username, self.password) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 000000000..55b2e8bdf --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,47 @@ +# ---------------------------------------------------------- +# Copyright (C) 2019 by +# ANSYS Switzerland GmbH +# www.ansys.com +# +# Author(s): O.Koenig +# ---------------------------------------------------------- +import logging +import time +import unittest + +from ansys.rep.client import Client +from tests.rep_test import REPTestCase + +log = logging.getLogger(__name__) + + +class REPClientTest(REPTestCase): + def test_authentication_workflows(self): + + client0 = Client(self.rep_url, self.username, self.password) + + self.assertTrue(client0.access_token is not None) + self.assertTrue(client0.refresh_token is not None) + + access_token0 = client0.access_token + refresh_token0 = client0.refresh_token + + # wait a second otherwise the OAuth server will issue the very same tokens + time.sleep(1) + + client0.refresh_access_token() + self.assertNotEqual(client0.access_token, access_token0) + self.assertNotEqual(client0.refresh_token, refresh_token0) + + client1 = Client(self.rep_url, access_token=client0.access_token) + self.assertEqual(client1.access_token, client0.access_token) + self.assertTrue(client1.refresh_token is None) + + client2 = Client(self.rep_url, refresh_token=client0.refresh_token) + self.assertTrue(client2.access_token is not None) + self.assertNotEqual(client2.refresh_token, client0.refresh_token) + client2.refresh_access_token() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/jms/test_connection.py b/tests/test_connection.py similarity index 100% rename from tests/jms/test_connection.py rename to tests/test_connection.py diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b0ded3d26..cb5136d29 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -8,8 +8,8 @@ import logging import unittest -from ansys.rep.client import APIError, ClientError -from ansys.rep.client.jms import Client +from ansys.rep.client import APIError, Client, ClientError +from ansys.rep.client.jms import JmsApi from tests.rep_test import REPTestCase log = logging.getLogger(__name__) @@ -18,11 +18,11 @@ class ExceptionTest(REPTestCase): def test_server_error(self): - client = self.jms_client() - + client = self.client() + jms_api = JmsApi(client) except_obj = None try: - client.get_projects(wrong_query_param="value") + jms_api.get_projects(wrong_query_param="value") except APIError as e: except_obj = e log.error(str(e)) @@ -48,8 +48,9 @@ def test_client_error(self): except_obj = None try: - client = self.jms_client() - client.get_project(id="02q4bg9PVO2OvvhsmClb0E") + client = self.client() + jms_api = JmsApi(client) + jms_api.get_project(id="02q4bg9PVO2OvvhsmClb0E") except ClientError as e: except_obj = e log.error(str(e))