diff --git a/.github/workflows/sdk-build.yml b/.github/workflows/sdk-build.yml index 5c8798b6..34e99474 100644 --- a/.github/workflows/sdk-build.yml +++ b/.github/workflows/sdk-build.yml @@ -29,15 +29,15 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt - name: Lint with pydocstyle - run: pydocstyle --match-dir='^(?!build$).*' --match='^(?!(__init__\.py|setup\.py$)).*\.py$' src + run: pydocstyle --match-dir='^(?!(build|response)$).*' --match='^(?!(__init__\.py|setup\.py$)).*\.py$' src - name: Lint with pydoclint - run: pydoclint --exclude='.*/build/.*' src + run: pydoclint --exclude='.*/(build|response)/.*' src - name: Lint with pylint run: | # check for Python errors pylint src --errors-only --disable=E0401,E0611 --ignore=build # check for lint - pylint ./src --disable=all --enable=C0103,C0301 --ignore=build --max-line-length=127 + pylint ./src --disable=all --enable=C0103,C0301 --ignore=build,response --max-line-length=127 - name: Check license headers run: python scripts/license_header.py src - name: Test with pytest diff --git a/.vscode/settings.json b/.vscode/settings.json index fe2a97f8..4ba0139c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,7 +17,8 @@ "./src/zos_console", "./src/zos_files", "./src/zos_jobs", - "./src/zosmf" + "./src/zosmf", + "./src/zos_tso" ], "python.linting.enabled": true, "python.linting.pylintEnabled": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index ee29df7a..1ff85eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to the Zowe Client Python SDK will be documented in this file. +## Recent Changes + +### Enhancements + +- *Breaking*: Update method return types to use custom classes for REST API responses [#89] (https://github.com/zowe/zowe-client-python-sdk/issues/89) + +### Bug Fixes + + ## `1.0.0-dev19` ### Enhancements diff --git a/scripts/license_header.py b/scripts/license_header.py index be195de6..9384aa7d 100644 --- a/scripts/license_header.py +++ b/scripts/license_header.py @@ -40,7 +40,7 @@ def main(): if "build" in root.split(os.path.sep): continue for file in files: - if file.endswith(".py"): + if file.endswith(".py") and file is not "_version.py": file_path = os.path.join(root, file) if not check_and_add_license_header(file_path, write_header): print(f"License header missing in: {file_path}") diff --git a/src/_version.py b/src/_version.py index f38cccec..610fd61e 100644 --- a/src/_version.py +++ b/src/_version.py @@ -1 +1,12 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" __version__ = "1.0.0-dev19" diff --git a/src/core/zowe/core_for_zowe_sdk/request_handler.py b/src/core/zowe/core_for_zowe_sdk/request_handler.py index e36aafec..3a8d6680 100644 --- a/src/core/zowe/core_for_zowe_sdk/request_handler.py +++ b/src/core/zowe/core_for_zowe_sdk/request_handler.py @@ -44,8 +44,8 @@ def __handle_ssl_warnings(self): urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def perform_request( - self, method: str, request_arguments: dict, expected_code: dict = [200], stream: bool = False - ) -> dict: + self, method: str, request_arguments: dict, expected_code: list = [200], stream: bool = False + ) -> Union[str, bytes, dict, None]: """Execute an HTTP/HTTPS requests from given arguments and return validated response (JSON). Parameters @@ -54,14 +54,14 @@ def perform_request( The request method that should be used request_arguments: dict The dictionary containing the required arguments for the execution of the request - expected_code: dict + expected_code: list The list containing the acceptable response codes (default is [200]) stream: bool The boolean value whether the request is stream Returns ------- - dict + Union[str, bytes, dict, None] normalized request response in json (dictionary) """ self.__method = method @@ -136,13 +136,13 @@ def __validate_response(self): ) raise RequestFailed(self.__response.status_code, output_str) - def __normalize_response(self) -> Union[str, bytes, dict]: + def __normalize_response(self) -> Union[str, bytes, dict, None]: """ Normalize the response object to a JSON format. Returns ------- - Union[str, bytes, dict] + Union[str, bytes, dict, None] Response object at the format based on Content-Type header: - `bytes` when the response is binary data - `str` when the response is plain text @@ -152,6 +152,6 @@ def __normalize_response(self) -> Union[str, bytes, dict]: if content_type == "application/octet-stream": return self.__response.content elif content_type and content_type.startswith("application/json"): - return "" if self.__response.text == "" else self.__response.json() + return None if self.__response.text == "" else self.__response.json() else: return self.__response.text diff --git a/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py b/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py index 9865cd8c..a7fdfb38 100644 --- a/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py +++ b/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py @@ -14,6 +14,8 @@ from zowe.core_for_zowe_sdk import SdkApi +from .response import ConsoleResponse, IssueCommandResponse + class Console(SdkApi): """ @@ -28,7 +30,7 @@ class Console(SdkApi): def __init__(self, connection: dict): super().__init__(connection, "/zosmf/restconsoles/consoles/defcn", logger_name=__name__) - def issue_command(self, command: str, console: Optional[str] = None) -> dict: + def issue_command(self, command: str, console: Optional[str] = None) -> IssueCommandResponse: """Issues a command on z/OS Console. Parameters @@ -40,7 +42,7 @@ def issue_command(self, command: str, console: Optional[str] = None) -> dict: Returns ------- - dict + IssueCommandResponse A JSON containing the response from the console command """ custom_args = self._create_custom_request_arguments() @@ -48,9 +50,9 @@ def issue_command(self, command: str, console: Optional[str] = None) -> dict: request_body = {"cmd": command} custom_args["json"] = request_body response_json = self.request_handler.perform_request("PUT", custom_args) - return response_json + return IssueCommandResponse(response_json) - def get_response(self, response_key: str, console: Optional[str] = None) -> dict: + def get_response(self, response_key: str, console: Optional[str] = None) -> ConsoleResponse: """ Collect outstanding synchronous z/OS Console response messages. @@ -63,11 +65,11 @@ def get_response(self, response_key: str, console: Optional[str] = None) -> dict Returns ------- - dict + ConsoleResponse A JSON containing the response to the command """ custom_args = self._create_custom_request_arguments() request_url = "{}/solmsgs/{}".format(console or "defcn", response_key) custom_args["url"] = self._request_endpoint.replace("defcn", request_url) response_json = self.request_handler.perform_request("GET", custom_args) - return response_json + return ConsoleResponse(response_json) diff --git a/src/zos_console/zowe/zos_console_for_zowe_sdk/response/__init__.py b/src/zos_console/zowe/zos_console_for_zowe_sdk/response/__init__.py new file mode 100644 index 00000000..6a6658ab --- /dev/null +++ b/src/zos_console/zowe/zos_console_for_zowe_sdk/response/__init__.py @@ -0,0 +1,13 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" + +from .console import ConsoleResponse, IssueCommandResponse diff --git a/src/zos_console/zowe/zos_console_for_zowe_sdk/response/console.py b/src/zos_console/zowe/zos_console_for_zowe_sdk/response/console.py new file mode 100644 index 00000000..33525e00 --- /dev/null +++ b/src/zos_console/zowe/zos_console_for_zowe_sdk/response/console.py @@ -0,0 +1,50 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" + +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass +class IssueCommandResponse: + cmd_response_key: Optional[str] = None + cmd_response_url: Optional[str] = None + cmd_response_uri: Optional[str] = None + cmd_response: Optional[str] = None + + def __init__(self, response: dict) -> None: + for k, value in response.items(): + key = k.replace("-", "_") + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> str: + return self.__dict__[key.replace("-", "_")] + + def __setitem__(self, key: str, value: str) -> None: + self.__dict__[key.replace("-", "_")] = value + + +@dataclass +class ConsoleResponse: + cmd_response: Optional[str] = None + sol_key_detected: Optional[bool] = None + + def __init__(self, response: dict) -> None: + for k, value in response.items(): + key = k.replace("-", "_") + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key.replace("-", "_")] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key.replace("-", "_")] = value diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/datasets.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/datasets.py index f1f4564d..6e2d120b 100644 --- a/src/zos_files/zowe/zos_files_for_zowe_sdk/datasets.py +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/datasets.py @@ -17,6 +17,7 @@ from zowe.core_for_zowe_sdk import SdkApi from zowe.core_for_zowe_sdk.exceptions import FileNotFound from zowe.zos_files_for_zowe_sdk.constants import FileType, zos_file_constants +from zowe.zos_files_for_zowe_sdk.response import DatasetListResponse, MemberListResponse _ZOWE_FILES_DEFAULT_ENCODING = zos_file_constants["ZoweFilesDefaultEncoding"] @@ -306,7 +307,7 @@ def __init__(self, connection: dict): super().__init__(connection, "/zosmf/restfiles/", logger_name=__name__) self._default_headers["Accept-Encoding"] = "gzip" - def list(self, name_pattern: str, return_attributes: bool = False) -> List[Dict]: + def list(self, name_pattern: str, return_attributes: bool = False) -> DatasetListResponse: """ Retrieve a list of datasets based on a given pattern. @@ -319,7 +320,7 @@ def list(self, name_pattern: str, return_attributes: bool = False) -> List[Dict] Returns ------- - List[Dict] + DatasetListResponse A JSON with a list of dataset names (and attributes if specified) matching the given pattern. """ custom_args = self._create_custom_request_arguments() @@ -330,7 +331,7 @@ def list(self, name_pattern: str, return_attributes: bool = False) -> List[Dict] custom_args["headers"]["X-IBM-Attributes"] = "base" response_json = self.request_handler.perform_request("GET", custom_args) - return response_json + return DatasetListResponse(response_json, return_attributes) def list_members( self, @@ -339,7 +340,7 @@ def list_members( member_start: Optional[str] = None, limit: int = 1000, attributes: str = "member", - ) -> dict: + ) -> MemberListResponse: """ Retrieve the list of members on a given PDS/PDSE. @@ -358,7 +359,7 @@ def list_members( Returns ------- - dict + MemberListResponse A JSON with a list of members from a given PDS/PDSE """ custom_args = self._create_custom_request_arguments() @@ -372,7 +373,7 @@ def list_members( custom_args["headers"]["X-IBM-Max-Items"] = "{}".format(limit) custom_args["headers"]["X-IBM-Attributes"] = attributes response_json = self.request_handler.perform_request("GET", custom_args) - return response_json["items"] # type: ignore + return MemberListResponse(response_json, (attributes == "base")) def copy_data_set_or_member( self, diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/file_system.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/file_system.py index f0a8aaa9..eb0bc99f 100644 --- a/src/zos_files/zowe/zos_files_for_zowe_sdk/file_system.py +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/file_system.py @@ -15,6 +15,8 @@ from zowe.core_for_zowe_sdk import SdkApi from zowe.zos_files_for_zowe_sdk import constants, exceptions +from .response import FileSystemListResponse + _ZOWE_FILES_DEFAULT_ENCODING = constants.zos_file_constants["ZoweFilesDefaultEncoding"] @@ -150,7 +152,9 @@ def unmount(self, file_system_name: str, options: dict = {}, encoding: str = _ZO response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[204]) return response_json - def list(self, file_path_name: Optional[str] = None, file_system_name: Optional[str] = None) -> dict: + def list( + self, file_path_name: Optional[str] = None, file_system_name: Optional[str] = None + ) -> FileSystemListResponse: """ List all mounted filesystems. @@ -166,7 +170,7 @@ def list(self, file_path_name: Optional[str] = None, file_system_name: Optional[ Returns ------- - dict + FileSystemListResponse A JSON containing the result of the operation """ custom_args = self._create_custom_request_arguments() @@ -174,4 +178,4 @@ def list(self, file_path_name: Optional[str] = None, file_system_name: Optional[ custom_args["params"] = {"path": file_path_name, "fsname": file_system_name} custom_args["url"] = "{}mfs".format(self._request_endpoint) response_json = self.request_handler.perform_request("GET", custom_args, expected_code=[200]) - return response_json + return FileSystemListResponse(response_json) diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/response/__init__.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/response/__init__.py new file mode 100644 index 00000000..c52af03a --- /dev/null +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/response/__init__.py @@ -0,0 +1,14 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" +from .datasets import DatasetListResponse, MemberListResponse +from .file_system import FileSystemListResponse +from .uss import USSListResponse diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/response/datasets.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/response/datasets.py new file mode 100644 index 00000000..76fe92d3 --- /dev/null +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/response/datasets.py @@ -0,0 +1,131 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + + +@dataclass +class DatasetListResponse: + items: Optional[List[dict]] = None + returnedRows: Optional[int] = None + totalRows: Optional[int] = None + JSONversion: Optional[int] = None + + def __init__(self, response: dict, attributes: bool) -> None: + for key, value in response.items(): + if key == "items": + value = ( + [DatasetResponse(**x) for x in value] if attributes else [SimpleDatasetResponse(**x) for x in value] + ) + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class SimpleDatasetResponse: + dsname: Optional[str] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class DatasetResponse: + dsname: Optional[str] = None + blksz: Optional[str] = None + catnm: Optional[str] = None + cdate: Optional[str] = None + dev: Optional[str] = None + dsorg: Optional[str] = None + edate: Optional[str] = None + extx: Optional[str] = None + lrecl: Optional[str] = None + migr: Optional[str] = None + mvol: Optional[str] = None + ovf: Optional[str] = None + rdate: Optional[str] = None + recfm: Optional[str] = None + sizex: Optional[str] = None + spacu: Optional[str] = None + used: Optional[str] = None + vol: Optional[str] = None + vols: Optional[str] = None + dsntp: Optional[str] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class MemberListResponse: + items: Optional[List[dict]] = None + totalRows: Optional[int] = None + JSONversion: Optional[int] = None + + def __init__(self, response: dict, attributes: bool) -> None: + for key, value in response.items(): + if key == "items": + value = ( + [MemberResponse(**x) for x in value] if attributes else [SimpleMemberResponse(**x) for x in value] + ) + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class SimpleMemberResponse: + member: Optional[str] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class MemberResponse: + member: Optional[str] = None + vers: Optional[int] = None + mod: Optional[int] = None + c4date: Optional[str] = None + m4date: Optional[str] = None + cnorc: Optional[int] = None + inorc: Optional[int] = None + mnorc: Optional[int] = None + mtime: Optional[str] = None + msec: Optional[str] = None + user: Optional[str] = None + sclm: Optional[str] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/response/file_system.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/response/file_system.py new file mode 100644 index 00000000..30e41750 --- /dev/null +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/response/file_system.py @@ -0,0 +1,59 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + + +@dataclass +class FileSystemResponse: + name: Optional[str] = None + mountpoint: Optional[str] = None + fstname: Optional[str] = None + status: Optional[str] = None + mode: Optional[List[str]] = None + dev: Optional[int] = None + fstype: Optional[int] = None + bsize: Optional[int] = None + bavail: Optional[int] = None + blocks: Optional[int] = None + sysname: Optional[str] = None + readibc: Optional[int] = None + writeibc: Optional[int] = None + diribc: Optional[int] = None + mountparm: Optional[str] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class FileSystemListResponse: + items: Optional[List[FileSystemResponse]] = None + returnedRows: Optional[int] = None + totalRows: Optional[int] = None + JSONversion: Optional[int] = None + + def __init__(self, response: dict) -> None: + for key, value in response.items(): + if key == "items": + value = [FileSystemResponse(**x) for x in value] + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/response/uss.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/response/uss.py new file mode 100644 index 00000000..d535aa33 --- /dev/null +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/response/uss.py @@ -0,0 +1,52 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" + +from dataclasses import dataclass +from typing import Any, List, Optional + + +@dataclass +class USSResponse: + name: Optional[str] = None + mode: Optional[str] = None + size: Optional[int] = None + uid: Optional[int] = None + user: Optional[str] = None + gid: Optional[int] = None + group: Optional[str] = None + mtime: Optional[str] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class USSListResponse: + items: Optional[List[dict]] = None + returnedRows: Optional[int] = None + totalRows: Optional[int] = None + JSONversion: Optional[int] = None + + def __init__(self, response: dict) -> None: + for key, value in response.items(): + if key == "items": + value = [USSResponse(**x) for x in value] + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/uss.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/uss.py index 0eb72258..e694e807 100644 --- a/src/zos_files/zowe/zos_files_for_zowe_sdk/uss.py +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/uss.py @@ -17,6 +17,8 @@ from zowe.core_for_zowe_sdk.exceptions import FileNotFound from zowe.zos_files_for_zowe_sdk.constants import zos_file_constants +from .response import USSListResponse + _ZOWE_FILES_DEFAULT_ENCODING = zos_file_constants["ZoweFilesDefaultEncoding"] @@ -36,7 +38,7 @@ def __init__(self, connection: dict): super().__init__(connection, "/zosmf/restfiles/", logger_name=__name__) self._default_headers["Accept-Encoding"] = "gzip" - def list(self, path: str) -> dict: + def list(self, path: str) -> USSListResponse: """ Retrieve a list of USS files based on a given pattern. @@ -47,14 +49,14 @@ def list(self, path: str) -> dict: Returns ------- - dict - A JSON with a list of dataset names matching the given pattern + USSListResponse + A JSON with a list of file names matching the given pattern """ custom_args = self._create_custom_request_arguments() custom_args["params"] = {"path": path} custom_args["url"] = "{}fs".format(self._request_endpoint) response_json = self.request_handler.perform_request("GET", custom_args) - return response_json + return USSListResponse(response_json) def delete(self, filepath_name: str, recursive: bool = False) -> dict: """ diff --git a/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py index b73b4f13..65659d9e 100644 --- a/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py +++ b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py @@ -11,10 +11,12 @@ """ import os -from typing import Optional +from typing import List, Optional from zowe.core_for_zowe_sdk import SdkApi +from .response import JobResponse, SpoolResponse, StatusResponse + class Jobs(SdkApi): """ @@ -31,7 +33,7 @@ class Jobs(SdkApi): def __init__(self, connection: dict): super().__init__(connection, "/zosmf/restjobs/jobs/", logger_name=__name__) - def get_job_status(self, jobname: str, jobid: str) -> dict: + def get_job_status(self, jobname: str, jobid: str) -> JobResponse: """ Retrieve the status of a given job on JES. @@ -44,7 +46,7 @@ def get_job_status(self, jobname: str, jobid: str) -> dict: Returns ------- - dict + JobResponse A JSON object containing the status of the job on JES """ custom_args = self._create_custom_request_arguments() @@ -52,9 +54,9 @@ def get_job_status(self, jobname: str, jobid: str) -> dict: request_url = "{}{}".format(self._request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url response_json = self.request_handler.perform_request("GET", custom_args) - return response_json + return JobResponse(response_json) - def cancel_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> dict: + def cancel_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> StatusResponse: """ Cancel a job. @@ -75,7 +77,7 @@ def cancel_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> d Returns ------- - dict + StatusResponse A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): @@ -89,9 +91,9 @@ def cancel_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> d custom_args["json"] = {"request": "cancel", "version": modify_version} response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[202, 200]) - return response_json + return StatusResponse(response_json) - def delete_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> dict: + def delete_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> StatusResponse: """ Delete the given job on JES. @@ -112,7 +114,7 @@ def delete_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> d Returns ------- - dict + StatusResponse A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): @@ -126,9 +128,9 @@ def delete_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> d custom_args["headers"]["X-IBM-Job-Modify-Version"] = modify_version response_json = self.request_handler.perform_request("DELETE", custom_args, expected_code=[202, 200]) - return response_json + return StatusResponse(response_json) - def _issue_job_request(self, req: dict, jobname: str, jobid: str, modify_version: str) -> dict: + def _issue_job_request(self, req: dict, jobname: str, jobid: str, modify_version: str) -> StatusResponse: """ Issue a job request. @@ -146,7 +148,7 @@ def _issue_job_request(self, req: dict, jobname: str, jobid: str, modify_version Returns ------- - dict + StatusResponse A JSON object containing the result of the request execution """ custom_args = self._create_custom_request_arguments() @@ -158,9 +160,11 @@ def _issue_job_request(self, req: dict, jobname: str, jobid: str, modify_version custom_args["headers"]["X-IBM-Job-Modify-Version"] = modify_version response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[202, 200]) - return response_json + return StatusResponse(response_json) - def change_job_class(self, jobname: str, jobid: str, class_name: str, modify_version: str = "2.0") -> dict: + def change_job_class( + self, jobname: str, jobid: str, class_name: str, modify_version: str = "2.0" + ) -> StatusResponse: """ Change the job class. @@ -183,17 +187,17 @@ def change_job_class(self, jobname: str, jobid: str, class_name: str, modify_ver Returns ------- - dict + StatusResponse A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): self.logger.error('Accepted values for modify_version: "1.0" or "2.0"') raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') - response_json = self._issue_job_request({"class": class_name}, jobname, jobid, modify_version) - return response_json + response = self._issue_job_request({"class": class_name}, jobname, jobid, modify_version) + return response - def hold_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> dict: + def hold_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> StatusResponse: """ Hold the given job on JES. @@ -214,17 +218,17 @@ def hold_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> dic Returns ------- - dict + StatusResponse A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): self.logger.error('Accepted values for modify_version: "1.0" or "2.0"') raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') - response_json = self._issue_job_request({"request": "hold"}, jobname, jobid, modify_version) - return response_json + response = self._issue_job_request({"request": "hold"}, jobname, jobid, modify_version) + return response - def release_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> dict: + def release_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> StatusResponse: """ Release the given job on JES. @@ -245,15 +249,15 @@ def release_job(self, jobname: str, jobid: str, modify_version: str = "2.0") -> Returns ------- - dict + StatusResponse A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): self.logger.error('Modify version not accepted; Must be "1.0" or "2.0"') raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') - response_json = self._issue_job_request({"request": "release"}, jobname, jobid, modify_version) - return response_json + response = self._issue_job_request({"request": "release"}, jobname, jobid, modify_version) + return response def list_jobs( self, @@ -261,7 +265,7 @@ def list_jobs( prefix: str = "*", max_jobs: int = 1000, user_correlator: Optional[str] = None, - ) -> dict: + ) -> List[JobResponse]: """ Retrieve list of jobs on JES based on the provided arguments. @@ -278,8 +282,8 @@ def list_jobs( Returns ------- - dict - A JSON object containing a list of jobs on JES queue based on the given parameters + List[JobResponse] + A list of jobs on JES queue based on the given parameters """ custom_args = self._create_custom_request_arguments() params = {"prefix": prefix, "max-jobs": max_jobs} @@ -289,9 +293,12 @@ def list_jobs( params["user-correlator"] = user_correlator custom_args["params"] = params response_json = self.request_handler.perform_request("GET", custom_args) - return response_json + response = [] + for item in response_json: + response.append(JobResponse(item)) + return response - def submit_from_mainframe(self, jcl_path: str) -> dict: + def submit_from_mainframe(self, jcl_path: str) -> JobResponse: """ Submit a job from a given dataset. @@ -302,16 +309,16 @@ def submit_from_mainframe(self, jcl_path: str) -> dict: Returns ------- - dict + JobResponse A JSON object containing the result of the request execution """ custom_args = self._create_custom_request_arguments() request_body = {"file": "//'%s'" % jcl_path} custom_args["json"] = request_body response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[201]) - return response_json + return JobResponse(response_json) - def submit_from_local_file(self, jcl_path: str) -> dict: + def submit_from_local_file(self, jcl_path: str) -> JobResponse: """ Submit a job from local file. @@ -331,7 +338,7 @@ def submit_from_local_file(self, jcl_path: str) -> dict: Returns ------- - dict + JobResponse A JSON object containing the result of the request execution """ if os.path.isfile(jcl_path): @@ -342,7 +349,7 @@ def submit_from_local_file(self, jcl_path: str) -> dict: self.logger.error("Provided argument is not a file path {}".format(jcl_path)) raise FileNotFoundError("Provided argument is not a file path {}".format(jcl_path)) - def submit_plaintext(self, jcl: str) -> dict: + def submit_plaintext(self, jcl: str) -> JobResponse: """ Submit a job from plain text input. @@ -353,16 +360,16 @@ def submit_plaintext(self, jcl: str) -> dict: Returns ------- - dict + JobResponse A JSON object containing the result of the request execution """ custom_args = self._create_custom_request_arguments() custom_args["data"] = str(jcl) custom_args["headers"] = {"Content-Type": "text/plain", "X-CSRF-ZOSMF-HEADER": ""} response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[201]) - return response_json + return JobResponse(response_json) - def get_spool_files(self, correlator: str) -> dict: + def get_spool_files(self, correlator: str) -> List[SpoolResponse]: """ Retrieve the spool files for a job identified by the correlator. @@ -373,7 +380,7 @@ def get_spool_files(self, correlator: str) -> dict: Returns ------- - dict + List[SpoolResponse] A JSON object containing the result of the request execution """ custom_args = self._create_custom_request_arguments() @@ -381,9 +388,12 @@ def get_spool_files(self, correlator: str) -> dict: request_url = "{}{}".format(self._request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url response_json = self.request_handler.perform_request("GET", custom_args) - return response_json + response = [] + for item in response_json: + response.append(SpoolResponse(item)) + return response - def get_jcl_text(self, correlator: str) -> dict: + def get_jcl_text(self, correlator: str) -> str: """ Retrieve the input JCL text for job with specified correlator. @@ -394,8 +404,8 @@ def get_jcl_text(self, correlator: str) -> dict: Returns ------- - dict - A JSON object containing the result of the request execution + str + A str object containing the result of the request execution """ custom_args = self._create_custom_request_arguments() job_url = "{}/files/JCL/records".format(correlator) @@ -404,7 +414,7 @@ def get_jcl_text(self, correlator: str) -> dict: response_json = self.request_handler.perform_request("GET", custom_args) return response_json - def get_spool_file_contents(self, correlator: str, id: str) -> dict: + def get_spool_file_contents(self, correlator: str, id: str) -> str: """ Retrieve the contents of a single spool file from a job. @@ -418,8 +428,8 @@ def get_spool_file_contents(self, correlator: str, id: str) -> dict: Returns ------- - dict - A JSON object containing the result of the request execution + str + The contents of the spool file """ custom_args = self._create_custom_request_arguments() job_url = "{}/files/{}/records".format(correlator, id) diff --git a/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/response/__init__.py b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/response/__init__.py new file mode 100644 index 00000000..7a0b25c1 --- /dev/null +++ b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/response/__init__.py @@ -0,0 +1,12 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" +from .jobs import JobResponse, SpoolResponse, StatusResponse diff --git a/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/response/jobs.py b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/response/jobs.py new file mode 100644 index 00000000..4afbb9d8 --- /dev/null +++ b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/response/jobs.py @@ -0,0 +1,104 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + + +@dataclass +class JobResponse: + owner: Optional[str] = None + phase: Optional[int] = None + subsystem: Optional[str] = None + phase_name: Optional[str] = None + job_correlator: Optional[str] = None + type: Optional[str] = None + url: Optional[str] = None + jobid: Optional[str] = None + job_class: Optional[str] = None + files_url: Optional[str] = None + jobname: Optional[str] = None + status: Optional[str] = None + retcode: Optional[str] = None + + def __init__(self, response: dict) -> None: + for k, value in response.items(): + key = k.replace("-", "_") + if key == "class": + key = "job_class" + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + if key == "class": + key = "job_class" + return self.__dict__[key.replace("-", "_")] + + def __setitem__(self, key: str, value: Any) -> None: + if key == "class": + key = "job_class" + self.__dict__[key.replace("-", "_")] = value + + +@dataclass +class StatusResponse: + owner: Optional[str] = None + jobid: Optional[str] = None + job_correlator: Optional[str] = None + message: Optional[str] = None + original_jobid: Optional[str] = None + jobname: Optional[str] = None + status: Optional[int] = None + + def __init__(self, response: dict) -> None: + for k, value in response.items(): + key = k.replace("-", "_") + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key.replace("-", "_")] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key.replace("-", "_")] = value + + +@dataclass +class SpoolResponse: + recfm: Optional[str] = None + records_url: Optional[str] = None + stepname: Optional[str] = None + subsystem: Optional[str] = None + job_correlator: Optional[str] = None + byte_count: Optional[int] = None + lrecl: Optional[int] = None + jobid: Optional[str] = None + ddname: Optional[str] = None + id: Optional[int] = None + record_count: Optional[int] = None + job_class: Optional[str] = None + jobname: Optional[str] = None + procstep: Optional[str] = None + + def __init__(self, response: dict) -> None: + for k, value in response.items(): + key = k.replace("-", "_") + if key == "class": + key = "job_class" + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + if key == "class": + key = "job_class" + return self.__dict__[key.replace("-", "_")] + + def __setitem__(self, key: str, value: Any) -> None: + if key == "class": + key = "job_class" + self.__dict__[key.replace("-", "_")] = value diff --git a/src/zos_tso/zowe/zos_tso_for_zowe_sdk/response/__init__.py b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/response/__init__.py new file mode 100644 index 00000000..985b9b2a --- /dev/null +++ b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/response/__init__.py @@ -0,0 +1,12 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" +from .tso import EndResponse, IssueResponse, SendResponse, StartResponse diff --git a/src/zos_tso/zowe/zos_tso_for_zowe_sdk/response/tso.py b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/response/tso.py new file mode 100644 index 00000000..1e779700 --- /dev/null +++ b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/response/tso.py @@ -0,0 +1,76 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + + +@dataclass +class StartResponse: + servletKey: Optional[str] = None + queueID: Optional[str] = None + sessionID: Optional[str] = None + ver: Optional[str] = None + tsoData: Optional[List[dict]] = None + reused: Optional[bool] = None + timeout: Optional[bool] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class EndResponse: + servletKey: Optional[str] = None + ver: Optional[str] = None + reused: Optional[bool] = None + timeout: Optional[bool] = None + msgData: Optional[str] = None + msgId: Optional[List] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class SendResponse: + servletKey: Optional[str] = None + ver: Optional[str] = None + tsoData: Optional[List[dict]] = None + reused: Optional[bool] = None + timeout: Optional[bool] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class IssueResponse: + start_response: StartResponse + send_response: SendResponse + end_response: EndResponse + tso_messages: list + + def __init__(self, start, send, end, msg): + self.start_response = start + self.send_response = send + self.end_response = end + self.tso_messages = msg diff --git a/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py index d8968471..f67f7c15 100644 --- a/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py +++ b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py @@ -15,6 +15,8 @@ from zowe.core_for_zowe_sdk import SdkApi, constants +from .response import EndResponse, IssueResponse, SendResponse, StartResponse + class Tso(SdkApi): """ @@ -33,7 +35,7 @@ def __init__(self, connection: dict, tso_profile: Optional[dict] = None): self.session_not_found = constants["TsoSessionNotFound"] self.tso_profile = tso_profile or {} - def issue_command(self, command: str) -> list: + def issue_command(self, command: str) -> IssueResponse: """ Issue a TSO command. @@ -47,17 +49,19 @@ def issue_command(self, command: str) -> list: Returns ------- - list + IssueResponse A list containing the output from the TSO command """ - session_key = self.start_tso_session() - command_output = self.send_tso_message(session_key, command) + start_response = self.start() + session_key = start_response.servletKey + send_response = self.send(session_key, command) + command_output = send_response.tsoData tso_messages = self.retrieve_tso_messages(command_output) while not any("TSO PROMPT" in message for message in command_output) or not tso_messages: command_output = self.__get_tso_data(session_key) tso_messages += self.retrieve_tso_messages(command_output) - self.end_tso_session(session_key) - return tso_messages + end_response = self.end(session_key) + return IssueResponse(start_response, send_response, end_response, tso_messages) def start_tso_session( self, @@ -93,6 +97,43 @@ def start_tso_session( str The 'servletKey' key for the created session (if successful) """ + return self.start(proc, chset, cpage, rows, cols, rsize, acct).servletKey + + def start( + self, + proc: Optional[str] = None, + chset: Optional[str] = None, + cpage: Optional[str] = None, + rows: Optional[str] = None, + cols: Optional[str] = None, + rsize: Optional[str] = None, + acct: Optional[str] = None, + ) -> StartResponse: + """ + Start a TSO session. + + Parameters + ---------- + proc: Optional[str] + Proc parameter for the TSO session (default is "IZUFPROC") + chset: Optional[str] + Chset parameter for the TSO session (default is "697") + cpage: Optional[str] + Cpage parameter for the TSO session (default is "1047") + rows: Optional[str] + Rows parameter for the TSO session (default is "204") + cols: Optional[str] + Cols parameter for the TSO session (default is "160") + rsize: Optional[str] + Rsize parameter for the TSO session (default is "4096") + acct: Optional[str] + Acct parameter for the TSO session (default is "DEFAULT") + + Returns + ------- + StartResponse + The 'servletKey' key for the created session (if successful) + """ custom_args = self._create_custom_request_arguments() custom_args["params"] = { "proc": proc or self.tso_profile.get("logonProcedure", "IZUFPROC"), @@ -104,7 +145,7 @@ def start_tso_session( "acct": acct or self.tso_profile.get("account", "DEFAULT"), } response_json = self.request_handler.perform_request("POST", custom_args) - return response_json["servletKey"] + return StartResponse(**response_json) def send_tso_message(self, session_key: str, message: str) -> list: """ @@ -122,13 +163,31 @@ def send_tso_message(self, session_key: str, message: str) -> list: list A non-normalized list from TSO containing the result from the command """ + return self.send(session_key, message).tsoData + + def send(self, session_key: str, message: str) -> SendResponse: + """ + Send a command to an existing TSO session. + + Parameters + ---------- + session_key: str + The session key of an existing TSO session + message: str + The message/command to be sent to the TSO session + + Returns + ------- + SendResponse + A non-normalized list from TSO containing the result from the command + """ custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}/{}".format(self._request_endpoint, str(session_key)) # z/OSMF TSO API requires json to be formatted in specific way without spaces request_json = {"TSO RESPONSE": {"VERSION": "0100", "DATA": str(message)}} custom_args["data"] = json.dumps(request_json, separators=(",", ":")) response_json = self.request_handler.perform_request("PUT", custom_args) - return response_json["tsoData"] + return SendResponse(**response_json) def ping_tso_session(self, session_key: str) -> str: """ @@ -165,11 +224,29 @@ def end_tso_session(self, session_key: str) -> str: str A string informing if the session was terminated successfully or not """ + message_id_list = self.end(session_key).msgId + return "Session ended" if self.session_not_found not in message_id_list else "Session already ended" + + def end(self, session_key: str) -> EndResponse: + """ + Terminates an existing TSO session. + + Parameters + ---------- + session_key: str + The session key of an existing TSO session + + Returns + ------- + EndResponse + A string informing if the session was terminated successfully or not + """ custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}/{}".format(self._request_endpoint, session_key) response_json = self.request_handler.perform_request("DELETE", custom_args) - message_id_list = self.parse_message_ids(response_json) - return "Session ended" if self.session_not_found not in message_id_list else "Session already ended" + response = EndResponse(**response_json) + response.msgId = self.parse_message_ids(response_json) + return response def parse_message_ids(self, response_json: dict) -> list: """ diff --git a/src/zosmf/zowe/zosmf_for_zowe_sdk/response/__init__.py b/src/zosmf/zowe/zosmf_for_zowe_sdk/response/__init__.py new file mode 100644 index 00000000..150ab151 --- /dev/null +++ b/src/zosmf/zowe/zosmf_for_zowe_sdk/response/__init__.py @@ -0,0 +1,12 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" +from .zosmf import ZosmfResponse diff --git a/src/zosmf/zowe/zosmf_for_zowe_sdk/response/zosmf.py b/src/zosmf/zowe/zosmf_for_zowe_sdk/response/zosmf.py new file mode 100644 index 00000000..90aac30b --- /dev/null +++ b/src/zosmf/zowe/zosmf_for_zowe_sdk/response/zosmf.py @@ -0,0 +1,51 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" +from dataclasses import dataclass, field +from typing import Any, List, Optional + + +@dataclass +class Plugin: + pluginVersion: Optional[str] = None + pluginDefaultName: Optional[str] = None + pluginStatus: Optional[str] = None + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key] = value + + +@dataclass +class ZosmfResponse: + zos_version: Optional[str] = None + zosmf_port: Optional[str] = None + zosmf_version: Optional[str] = None + zosmf_hostname: Optional[str] = None + plugins: List[Plugin] = field(default_factory=list) + zosmf_saf_realm: Optional[str] = None + zosmf_full_version: Optional[str] = None + api_version: Optional[str] = None + + def __init__(self, response: dict) -> None: + for k, value in response.items(): + key = k.replace("-", "_") + if key == "plugins": + value = [Plugin(**x) for x in value] + super().__setattr__(key, value) + + def __getitem__(self, key: str) -> Any: + return self.__dict__[key.replace("-", "_")] + + def __setitem__(self, key: str, value: Any) -> None: + self.__dict__[key.replace("-", "_")] = value diff --git a/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py b/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py index 94a61389..05691827 100644 --- a/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py +++ b/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py @@ -12,6 +12,8 @@ from zowe.core_for_zowe_sdk import SdkApi +from .response import ZosmfResponse + class Zosmf(SdkApi): """ @@ -26,28 +28,28 @@ class Zosmf(SdkApi): def __init__(self, connection: dict): super().__init__(connection, "/zosmf/info", logger_name=__name__) - def get_info(self) -> dict: + def get_info(self) -> ZosmfResponse: """ Return a JSON response from the GET request to z/OSMF info endpoint. Returns ------- - dict + ZosmfResponse A JSON containing the z/OSMF Info REST API data """ response_json = self.request_handler.perform_request("GET", self._request_arguments) - return response_json + return ZosmfResponse(response_json) - def list_systems(self) -> dict: + def list_systems(self) -> ZosmfResponse: """ Return a JSON response from the GET request to z/OSMF info endpoint. Returns ------- - dict + ZosmfResponse Return a list of the systems that are defined to a z/OSMF instance """ custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}/systems".format(self._request_endpoint) response_json = self.request_handler.perform_request("GET", custom_args, expected_code=[200]) - return response_json + return ZosmfResponse(response_json) diff --git a/tests/integration/test_zos_console.py b/tests/integration/test_zos_console.py index 7f9340b8..a03dfb5a 100644 --- a/tests/integration/test_zos_console.py +++ b/tests/integration/test_zos_console.py @@ -24,4 +24,4 @@ def test_get_response_should_return_messages(self): """Test that response message can be received from the console""" command_output = self.console.issue_command("D T") response = self.console.get_response(command_output["cmd-response-key"]) - self.assertTrue("cmd-response" in response) + self.assertTrue(hasattr(response, "cmd_response")) diff --git a/tests/integration/test_zos_files.py b/tests/integration/test_zos_files.py index 8e68d1f3..29752582 100644 --- a/tests/integration/test_zos_files.py +++ b/tests/integration/test_zos_files.py @@ -7,6 +7,10 @@ import urllib3 from zowe.core_for_zowe_sdk import ProfileManager from zowe.zos_files_for_zowe_sdk import Files +from zowe.zos_files_for_zowe_sdk.response.datasets import ( + DatasetListResponse, + MemberListResponse, +) FIXTURES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures") FILES_FIXTURES_PATH = os.path.join(FIXTURES_PATH, "files.json") @@ -46,24 +50,24 @@ def test_list_dsn_should_return_a_list_of_datasets(self): command_output = self.files.list_dsn(self.files_fixtures["TEST_HLQ"], scenario["attributes"]) # Assert that command_output['items'] is a list - self.assertIsInstance(command_output["items"], list) + self.assertIsInstance(command_output, DatasetListResponse) # Assert that command_output['items'] contains at least one item self.assertGreater(len(command_output["items"]), 0) # Assert that the first item in the list has 'dsname' defined first_item = command_output["items"][0] - self.assertIn("dsname", first_item) + self.assertTrue(hasattr(first_item, "dsname")) # Assert that the first item in the list has the expected attributes defined - attributes = first_item.keys() + attributes = dir(first_item) for expected_attr in scenario["expected_attributes"]: self.assertIn(expected_attr, attributes) def test_list_members_should_return_a_list_of_members(self): """Executing list_dsn_members should return a list of members.""" command_output = self.files.list_dsn_members(self.files_fixtures["TEST_PDS"]) - self.assertIsInstance(command_output, list) + self.assertIsInstance(command_output, MemberListResponse) def test_get_dsn_content_should_return_content_from_dataset(self): """Executing get_dsn_content should return content from dataset.""" @@ -88,14 +92,14 @@ def test_get_file_content_streamed_should_return_response_content(self): def test_write_to_dsn_should_be_possible(self): """Executing write_to_dsn should be possible.""" command_output = self.files.write_to_dsn(self.test_member_generic, "HELLO WORLD") - self.assertTrue(command_output == "") + self.assertTrue(command_output == None) def test_copy_uss_to_data_set_should_be_possible(self): """Executing copy_uss_to_data_set should be possible.""" command_output = self.files.copy_uss_to_data_set( self.files_fixtures["TEST_USS"], self.files_fixtures["TEST_PDS"] + "(TEST2)", replace=True ) - self.assertTrue(command_output == "") + self.assertTrue(command_output == None) def test_copy_data_set_or_member_should_be_possible(self): """Executing copy_data_set_or_member should be possible.""" @@ -107,7 +111,7 @@ def test_copy_data_set_or_member_should_be_possible(self): "replace": True, } command_output = self.files.copy_data_set_or_member(**test_case) - self.assertTrue(command_output == "") + self.assertTrue(command_output == None) def test_mount_unmount_zfs_file_system(self): """Mounting a zfs filesystem should be possible""" @@ -121,7 +125,7 @@ def test_mount_unmount_zfs_file_system(self): command_output = self.files.mount_file_system( self.test2_zfs_file_system, mount_point, self.mount_zfs_file_system_options ) - self.assertTrue(command_output == "") + self.assertTrue(command_output == None) # List a zfs file system command_output = self.files.list_unix_file_systems(file_system_name=self.test2_zfs_file_system.upper()) @@ -129,11 +133,11 @@ def test_mount_unmount_zfs_file_system(self): # Unmount file system command_output = self.files.unmount_file_system(self.test2_zfs_file_system) - self.assertTrue(command_output == "") + self.assertTrue(command_output == None) # Delete file system command_output = self.files.delete_zfs_file_system(self.test2_zfs_file_system) - self.assertTrue(command_output == "") + self.assertTrue(command_output == None) def test_upload_download_delete_dataset(self): self.files.upload_file_to_dsn(SAMPLE_JCL_FIXTURE_PATH, self.test_ds_upload) diff --git a/tests/integration/test_zos_tso.py b/tests/integration/test_zos_tso.py index 81191f49..c9a08854 100644 --- a/tests/integration/test_zos_tso.py +++ b/tests/integration/test_zos_tso.py @@ -4,6 +4,7 @@ from zowe.core_for_zowe_sdk import ProfileManager from zowe.zos_tso_for_zowe_sdk import Tso +from zowe.zos_tso_for_zowe_sdk.response import IssueResponse class TestTsoIntegration(unittest.TestCase): @@ -18,8 +19,8 @@ def setUp(self): def test_issue_command_should_return_valid_response(self): """Executing the issue_command method should return a valid response from TSO""" command_output = self.tso.issue_command("TIME") - self.assertIsInstance(command_output, list) - self.assertIn("TIME", command_output[0]) + self.assertIsInstance(command_output, IssueResponse) + self.assertIn("TIME", command_output.tso_messages[0]) def test_start_tso_session_should_return_a_session_key(self): """Executing the start_tso_session method should return a valid TSO session key""" diff --git a/tests/integration/test_zosmf.py b/tests/integration/test_zosmf.py index 70f0e785..1fc9a1be 100644 --- a/tests/integration/test_zosmf.py +++ b/tests/integration/test_zosmf.py @@ -1,8 +1,10 @@ """Integration tests for the Zowe Python SDK z/OSMF package.""" + import unittest from zowe.core_for_zowe_sdk import ProfileManager from zowe.zosmf_for_zowe_sdk import Zosmf +from zowe.zosmf_for_zowe_sdk.response import ZosmfResponse class TestZosmfIntegration(unittest.TestCase): @@ -17,9 +19,9 @@ def setUp(self): def test_get_info_should_return_valid_response(self): """Executing the get_info method should return a valid response.""" command_output = self.zosmf.get_info() - self.assertIsInstance(command_output, dict) + self.assertIsInstance(command_output, ZosmfResponse) def test_list_systems_should_return_valid_response(self): """Executing the list_systems method should return a valid response.""" command_output = self.zosmf.list_systems() - self.assertIsInstance(command_output, dict) + self.assertIsInstance(command_output, ZosmfResponse) diff --git a/tests/unit/core/test_request_handler.py b/tests/unit/core/test_request_handler.py index de9c7ebe..fe2364b5 100644 --- a/tests/unit/core/test_request_handler.py +++ b/tests/unit/core/test_request_handler.py @@ -75,4 +75,4 @@ def test_empty_text(self, mock_send_request): ) request_handler = RequestHandler(self.session_arguments) response = request_handler.perform_request("GET", {"url": "https://www.zowe.org"}) - self.assertTrue(response == "") + self.assertTrue(response == None) diff --git a/tests/unit/files/datasets/test_list.py b/tests/unit/files/datasets/test_list.py index 3e6d1349..1018c851 100644 --- a/tests/unit/files/datasets/test_list.py +++ b/tests/unit/files/datasets/test_list.py @@ -1,8 +1,9 @@ """Unit tests for the Zowe Python SDK z/OS Files package.""" + import re from unittest import TestCase, mock -from zowe.zos_files_for_zowe_sdk import Files, exceptions, Datasets +from zowe.zos_files_for_zowe_sdk import Datasets, Files, exceptions class TestFilesClass(TestCase): @@ -21,7 +22,11 @@ def setUp(self): @mock.patch("requests.Session.send") def test_list_dsn(self, mock_send_request): """Test list DSN sends request""" - mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response test_values = [("MY.DSN", False), ("MY.DSN", True)] for test_case in test_values: @@ -33,22 +38,21 @@ def test_list_members(self, mock_send_request): """Test list members sends request""" self.files_instance = Files(self.test_profile) mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) - mock_send_request.return_value.json.return_value = {"items": ["MEMBER1", "MEMBER2"]} + mock_send_request.return_value.json.return_value = {"items": [{}, {}]} test_cases = [ ("MY.PDS", None, None, 1000, "member"), ("MY.PDS", "MEM*", None, 1000, "member"), ("MY.PDS", None, "MEMBER1", 1000, "member"), - ("MY.PDS", "MEM*", "MEMBER1", 500, "extended") + ("MY.PDS", "MEM*", "MEMBER1", 500, "extended"), ] for dataset_name, member_pattern, member_start, limit, attributes in test_cases: result = self.files_instance.list_dsn_members(dataset_name, member_pattern, member_start, limit, attributes) - self.assertEqual(result, ["MEMBER1", "MEMBER2"]) mock_send_request.assert_called() prepared_request = mock_send_request.call_args[0][0] self.assertEqual(prepared_request.method, "GET") self.assertIn(dataset_name, prepared_request.url) self.assertEqual(prepared_request.headers["X-IBM-Max-Items"], str(limit)) - self.assertEqual(prepared_request.headers["X-IBM-Attributes"], attributes) \ No newline at end of file + self.assertEqual(prepared_request.headers["X-IBM-Attributes"], attributes) diff --git a/tests/unit/files/file_systems/test_file_systems.py b/tests/unit/files/file_systems/test_file_systems.py index 1badb1b9..58a8dd26 100644 --- a/tests/unit/files/file_systems/test_file_systems.py +++ b/tests/unit/files/file_systems/test_file_systems.py @@ -68,3 +68,15 @@ def test_unmount_zFS_file_system(self, mock_send_request): Files(self.test_profile).unmount_file_system("file_system_name") mock_send_request.assert_called_once() + + @mock.patch("requests.Session.send") + def test_list_fs(self, mock_send_request): + """Test list DSN sends request""" + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response + + Files(self.test_profile).list_unix_file_systems() + mock_send_request.assert_called() diff --git a/tests/unit/files/uss/test_uss.py b/tests/unit/files/uss/test_uss.py index 3a9ab204..6a900967 100644 --- a/tests/unit/files/uss/test_uss.py +++ b/tests/unit/files/uss/test_uss.py @@ -1,8 +1,9 @@ """Unit tests for the Zowe Python SDK z/OS Files package.""" + import re from unittest import TestCase, mock -from zowe.zos_files_for_zowe_sdk import Files, exceptions, Datasets +from zowe.zos_files_for_zowe_sdk import Datasets, Files, exceptions class TestFilesClass(TestCase): @@ -17,7 +18,7 @@ def setUp(self): "port": 443, "rejectUnauthorized": True, } - + @mock.patch("requests.Session.send") def test_delete_uss(self, mock_send_request): """Test deleting a directory recursively sends a request""" @@ -55,4 +56,16 @@ def test_write(self, mock_send_request): Files(self.test_profile).write_to_uss(filepath_name="test", data="test") mock_send_request.assert_called_once() prepared_request = mock_send_request.call_args[0][0] - self.assertEqual(prepared_request.method, "PUT") \ No newline at end of file + self.assertEqual(prepared_request.method, "PUT") + + @mock.patch("requests.Session.send") + def test_list_uss(self, mock_send_request): + """Test list DSN sends request""" + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response + + Files(self.test_profile).list_files("") + mock_send_request.assert_called() diff --git a/tests/unit/test_zos_console.py b/tests/unit/test_zos_console.py index 52e05ac2..460bf220 100644 --- a/tests/unit/test_zos_console.py +++ b/tests/unit/test_zos_console.py @@ -27,26 +27,40 @@ def test_object_should_be_instance_of_class(self): @mock.patch("requests.Session.send") def test_issue_command_makes_request_to_the_default_console(self, mock_send): """Issued command should be sent to the correct default console name if no name is specified""" - is_console_name_correct = False + def send_request_side_effect(self, **other_args): assert "/defcn" in self.url - return mock.Mock(headers={"Content-type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + return mock_response + mock_send.side_effect = send_request_side_effect Console(self.session_details).issue_command("TESTCMD") @mock.patch("requests.Session.send") def test_issue_command_makes_request_to_the_custom_console(self, mock_send): """Issued command should be sent to the correct custom console name if the console name is specified""" - is_console_name_correct = False + def send_request_side_effect(self, **other_args): assert "/TESTCNSL" in self.url - return mock.Mock(headers={"Content-type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + return mock_response + mock_send.side_effect = send_request_side_effect Console(self.session_details).issue_command("TESTCMD", "TESTCNSL") @mock.patch("requests.Session.send") def test_get_response_should_return_messages(self, mock_send_request): """Getting z/OS Console response messages on sending a response key""" - mock_send_request.return_value = mock.Mock(headers={"Content-type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response Console(self.session_details).get_response("console-key") mock_send_request.assert_called_once() diff --git a/tests/unit/test_zos_jobs.py b/tests/unit/test_zos_jobs.py index 5bb93528..44794c65 100644 --- a/tests/unit/test_zos_jobs.py +++ b/tests/unit/test_zos_jobs.py @@ -26,7 +26,11 @@ def test_object_should_be_instance_of_class(self): @mock.patch("requests.Session.send") def test_cancel_job(self, mock_send_request): """Test cancelling a job sends a request""" - mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response Jobs(self.test_profile).cancel_job("TESTJOB2", "JOB00084") mock_send_request.assert_called_once() @@ -34,7 +38,11 @@ def test_cancel_job(self, mock_send_request): @mock.patch("requests.Session.send") def test_hold_job(self, mock_send_request): """Test holding a job sends a request""" - mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response Jobs(self.test_profile).hold_job("TESTJOB2", "JOB00084") mock_send_request.assert_called_once() @@ -50,7 +58,11 @@ def test_modified_version_hold_job(self, mock_send_request): @mock.patch("requests.Session.send") def test_modified_version_release_job(self, mock_send_request): """Test holding a job sends a request""" - mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response with self.assertRaises(ValueError): Jobs(self.test_profile).release_job("TESTJOB2", "JOB00084", modify_version="3.0") @@ -58,7 +70,11 @@ def test_modified_version_release_job(self, mock_send_request): @mock.patch("requests.Session.send") def test_release_job(self, mock_send_request): """Test releasing a job sends a request""" - mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response Jobs(self.test_profile).release_job("TESTJOB2", "JOB00084") mock_send_request.assert_called_once() @@ -66,7 +82,11 @@ def test_release_job(self, mock_send_request): @mock.patch("requests.Session.send") def test_change_job_class(self, mock_send_request): """Test changing the job class sends a request""" - mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response Jobs(self.test_profile).change_job_class("TESTJOB2", "JOB00084", "A") mock_send_request.assert_called_once() @@ -92,7 +112,9 @@ def test_cancel_job_modify_version_parameterized(self): jobs_test_object = Jobs(self.test_profile) for test_case in test_values: - jobs_test_object.request_handler.perform_request = mock.Mock() + mock_response = mock.Mock() + mock_response.json.return_value = {} + jobs_test_object.request_handler.perform_request = mock_response.json if test_case[1]: jobs_test_object.cancel_job(*test_case[0]) diff --git a/tests/unit/test_zos_tso.py b/tests/unit/test_zos_tso.py index 27567e56..3b6273d9 100644 --- a/tests/unit/test_zos_tso.py +++ b/tests/unit/test_zos_tso.py @@ -26,24 +26,39 @@ def test_object_should_be_instance_of_class(self): @mock.patch("requests.Session.send") def test_issue_command(self, mock_send_request): """Test issuing a command sends a request""" - expected = ['READY', 'GO'] - message = {"TSO MESSAGE": { - "DATA": expected[0] - } - } - message2 = {"TSO MESSAGE": { - "DATA": expected[1] - } - } + expected = ["READY", "GO"] + message = {"TSO MESSAGE": {"DATA": expected[0]}} + message2 = {"TSO MESSAGE": {"DATA": expected[1]}} fake_responses = [ - mock.Mock(headers={"Content-Type": "application/json"}, status_code=200, json=lambda: {"servletKey": None, "tsoData": [ message]}), - mock.Mock(headers={"Content-Type": "application/json"}, status_code=200, json=lambda: {"servletKey": None, "tsoData": [ message]}), - mock.Mock(headers={"Content-Type": "application/json"}, status_code=200, json=lambda: {"servletKey": None, "tsoData": ["TSO PROMPT", message2]}), - mock.Mock(headers={"Content-Type": "application/json"}, status_code=200, json=lambda: {"servletKey": None, "tsoData": [ message]}), + mock.Mock( + headers={"Content-Type": "application/json"}, + status_code=200, + json=lambda: {"servletKey": None, "tsoData": [message]}, + ), + mock.Mock( + headers={"Content-Type": "application/json"}, + status_code=200, + json=lambda: {"servletKey": None, "tsoData": [message]}, + ), + mock.Mock( + headers={"Content-Type": "application/json"}, + status_code=200, + json=lambda: {"servletKey": None, "tsoData": [message2]}, + ), + mock.Mock( + headers={"Content-Type": "application/json"}, + status_code=200, + json=lambda: {"servletKey": None, "tsoData": ["TSO PROMPT"]}, + ), + mock.Mock( + headers={"Content-Type": "application/json"}, + status_code=200, + json=lambda: {"servletKey": None}, + ), ] - + mock_send_request.side_effect = fake_responses - result = Tso(self.test_profile).issue_command("TIME") + result = Tso(self.test_profile).issue_command("TIME").tso_messages self.assertEqual(result, expected) - self.assertEqual(mock_send_request.call_count, 4) + self.assertEqual(mock_send_request.call_count, 5) diff --git a/tests/unit/test_zosmf.py b/tests/unit/test_zosmf.py index 91a81e10..2a0c2b07 100644 --- a/tests/unit/test_zosmf.py +++ b/tests/unit/test_zosmf.py @@ -27,6 +27,11 @@ def test_object_should_be_instance_of_class(self): @mock.patch("requests.Session.send") def test_list_systems(self, mock_send_request): """Listing z/OSMF systems should send a REST request""" - mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) + mock_response = mock.Mock() + mock_response.headers = {"Content-Type": "application/json"} + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_send_request.return_value = mock_response + Zosmf(self.connection_dict).list_systems() mock_send_request.assert_called_once()