diff --git a/libs/unity-py/CHANGELOG.md b/libs/unity-py/CHANGELOG.md index d289f5f3..6a245713 100644 --- a/libs/unity-py/CHANGELOG.md +++ b/libs/unity-py/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.0] - 2025-02-19 + +### Added +* Unit test to validate the health services schema + +### Fixed +* Schema definition of health service as it wasn't properly validating enumerated values + +### Changed +* The health services schema has been updated to account for the new fields, componentCategory, componentType, and description +* Improved error handling of health service methods related to fetching health information from API +* Unit tests have been updated to use mock data rather than live API endpoints +* Updated printing of health status report to include new fields mentioned above + +### Removed +* Superfluous unit test that creates a health_service instance + +### Security + +### Deprecated + ## [0.9.0] - 2025-02-19 ### Added diff --git a/libs/unity-py/pyproject.toml b/libs/unity-py/pyproject.toml index 33165d40..9099216e 100644 --- a/libs/unity-py/pyproject.toml +++ b/libs/unity-py/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "unity-sds-client" -version = "0.9.0" +version = "0.10.0" description = "Unity-Py is a Python client to simplify interactions with NASA's Unity Platform." authors = ["Anil Natha, Mike Gangl"] diff --git a/libs/unity-py/tests/test_files/health_api_mock_data.json b/libs/unity-py/tests/test_files/health_api_mock_data.json new file mode 100644 index 00000000..072dc538 --- /dev/null +++ b/libs/unity-py/tests/test_files/health_api_mock_data.json @@ -0,0 +1,68 @@ +{ + "services": [ + { + "componentName": "Airflow API", + "componentCategory": "general", + "componentType": "api", + "description": "", + "ssmKey": "/unity/unity/dev/component/luca/airflow-api", + "healthCheckUrl": "http://k8s-airflow-airflowi-c68f394a2a-1712638265.us-west-2.elb.amazonaws.com:5000/api/v1/health", + "landingPageUrl": "http://k8s-airflow-airflowi-c68f394a2a-1712638265.us-west-2.elb.amazonaws.com:5000/api/v1", + "healthChecks": [ + { + "status": "HEALTHY", + "httpResponseCode": "200", + "date": "2024-07-26T07:29:21.316116" + } + ] + }, + { + "componentName": "Airflow UI", + "componentCategory": "general", + "componentType": "ui", + "description": "", + "ssmKey": "/unity/unity/dev/component/luca/airflow-ui", + "healthCheckUrl": "http://k8s-airflow-airflowi-c68f394a2a-1712638265.us-west-2.elb.amazonaws.com:5000/health", + "landingPageUrl": "http://k8s-airflow-airflowi-c68f394a2a-1712638265.us-west-2.elb.amazonaws.com:5000", + "healthChecks": [ + { + "status": "HEALTHY", + "httpResponseCode": "200", + "date": "2024-07-26T07:29:21.363497" + } + ] + }, + { + "componentName": "OGC API", + "componentCategory": "general", + "componentType": "api", + "description": "", + "ssmKey": "/unity/unity/dev/component/luca/ogc-api", + "healthCheckUrl": "http://k8s-airflow-ogcproce-e84448018d-906922971.us-west-2.elb.amazonaws.com:5001/health", + "landingPageUrl": "http://k8s-airflow-ogcproce-e84448018d-906922971.us-west-2.elb.amazonaws.com:5001", + "healthChecks": [ + { + "status": "HEALTHY", + "httpResponseCode": "200", + "date": "2024-07-26T07:29:21.395126" + } + ] + }, + { + "componentName": "Management Console", + "componentCategory": "general", + "componentType": "ui", + "description": "", + "ssmKey": "/unity/unity/dev/component/management-console", + "healthCheckUrl": "http://unity-dev-httpd-alb-443241596.us-west-2.elb.amazonaws.com:8080/management/api/health_checks", + "landingPageUrl": "http://unity-dev-httpd-alb-443241596.us-west-2.elb.amazonaws.com:8080/management/ui", + "healthChecks": [ + { + "status": "UNHEALTHY", + "httpResponseCode": "404", + "date": "2024-07-26T07:29:21.417864" + } + ] + } + ] +} \ No newline at end of file diff --git a/libs/unity-py/tests/test_unity_health_service.py b/libs/unity-py/tests/test_unity_health_service.py index 2a7cc1db..30db14fd 100644 --- a/libs/unity-py/tests/test_unity_health_service.py +++ b/libs/unity-py/tests/test_unity_health_service.py @@ -3,6 +3,8 @@ Unity Health Service is functional. """ +from unittest.mock import patch, Mock + import json import pytest @@ -13,30 +15,20 @@ @pytest.mark.regression -def test_health_service_client_creation(): +def test_health_status_schema(): """ - Test that an instance of the health service can be instantiated. + Test that Health API schema is valid """ - s = Unity() - health_service = s.client(UnityServices.HEALTH_SERVICE) + print("Validate Health API Schema") -@pytest.mark.regression -def test_health_status_retrieval(): - """ - Test that health statuses can be retrieved using the health service. - """ - print("Example health status check") - s = Unity(environment=UnityEnvironments.DEV) - s.set_project("unity") - s.set_venue("dev") - health_service = s.client(UnityServices.HEALTH_SERVICE) - health_statuses = health_service.get_health_status() - f = open('../../schemas/health-service/health-services.schema.json', encoding='utf-8') - schema = json.load(f) + mock_data_file_path = 'tests/test_files/health_api_mock_data.json' + schema_file_path = '../../schemas/health-service/health-services.schema.json' - validate(instance=health_statuses, schema=schema) - - assert health_statuses is not None + with open(mock_data_file_path, encoding='utf-8') as f_mock_data, \ + open(schema_file_path, encoding='utf-8') as f_health_schema: + mock_health_data = json.load(f_mock_data) + schema = json.load(f_health_schema) + validate(instance=mock_health_data, schema=schema) @pytest.mark.regression def test_health_status_printing(): @@ -49,7 +41,14 @@ def test_health_status_printing(): health_service = s.client(UnityServices.HEALTH_SERVICE) print("Example health status output using health service object:") - health_service.print_health_status() + + mock_data_file_path = 'tests/test_files/health_api_mock_data.json' + with open(mock_data_file_path, encoding='utf-8') as f_mock_data: + mock_get_patcher = patch('unity_sds_client.services.health_service.requests.get') + mock_get = mock_get_patcher.start() + mock_get.return_value = Mock(status_code = 200) + mock_get.return_value.json.return_value = json.load(f_mock_data) + health_service.print_health_status() @pytest.mark.regression def test_health_service_printing(): @@ -61,4 +60,10 @@ def test_health_service_printing(): s.set_venue("dev") print("Example health status output using unity object:") - print(s) \ No newline at end of file + mock_data_file_path = 'tests/test_files/health_api_mock_data.json' + with open(mock_data_file_path, encoding='utf-8') as f_mock_data: + mock_get_patcher = patch('unity_sds_client.services.health_service.requests.get') + mock_get = mock_get_patcher.start() + mock_get.return_value = Mock(status_code = 200) + mock_get.return_value.json.return_value = json.load(f_mock_data) + print(s) diff --git a/libs/unity-py/unity_sds_client/services/health_service.py b/libs/unity-py/unity_sds_client/services/health_service.py index c1a7ae42..682d168a 100644 --- a/libs/unity-py/unity_sds_client/services/health_service.py +++ b/libs/unity-py/unity_sds_client/services/health_service.py @@ -51,7 +51,12 @@ def get_health_status(self): headers = get_headers(token, { 'Content-type': 'application/json' }) - response = requests.get(url, headers=headers, timeout=60) + + try: + response = requests.get(url, headers=headers, timeout=60) + response.raise_for_status() + except requests.HTTPError as exception: + raise exception return response.json() @@ -60,21 +65,33 @@ def generate_health_status_report(self): Return a generated report of health status information """ - health_statuses = self.get_health_status() - health_status_title = "HEALTH STATUS REPORT" report = f"\n\n{health_status_title}\n" report = report + len(health_status_title) * "-" + "\n\n" + + try: + health_statuses = self.get_health_status() + except requests.HTTPError as error: + report = report + f"Error encountered with Health API Endpoint ({error.response.status_code})\n" + return report + for service in health_statuses["services"]: service_name = service["componentName"] + service_category = service["componentCategory"] + service_type = service["componentType"] + service_description = service["description"] landing_page_url = service["landingPageUrl"] - report = report + f"{service_name} ({landing_page_url})\n" + report = report + f"{service_name}\n" + report = report + f"{service_description}\n" + report = report + f"URL: {landing_page_url}\n" + report = report + f"Category: {service_category}\n" + report = report + f"Type: {service_type}\n" for status in service["healthChecks"]: service_status = status["status"] service_status_date = status["date"] - report = report + f"{service_status_date}: {service_status}\n" + report = report + f"Health Status as of {service_status_date}: {service_status}\n" report = report + "\n" - + return report def print_health_status(self): diff --git a/schemas/health-service/health-services.schema.json b/schemas/health-service/health-services.schema.json index 33d2b3db..8c758371 100644 --- a/schemas/health-service/health-services.schema.json +++ b/schemas/health-service/health-services.schema.json @@ -4,58 +4,69 @@ "properties": { "services": { "type": "array", - "items": [ - { - "type": "object", - "properties": { - "componentName": { - "type": "string" - }, - "ssmKey": { - "type": "string" - }, - "healthCheckUrl": { - "type": "string" - }, - "landingPageUrl": { - "type": "string" - }, - "healthChecks": { - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["HEALTHY", "UNHEALTHY"] - }, - "httpResponseCode": { - "type": "string" - }, - "date": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "status", - "httpResponseCode", - "date" - ] - } - ] - } + "items": { + "type": "object", + "properties": { + "componentName": { + "type": "string" + }, + "componentCategory": { + "type": "string", + "enum": ["administration", "catalogs", "development", "infrastructure", "processing", "general"] + }, + "componentType": { + "type": "string", + "enum": ["api", "ui", "unknown"] + }, + "description": { + "type": "string" + }, + "ssmKey": { + "type": "string" }, - "required": [ - "componentName", - "ssmKey", - "healthCheckUrl", - "landingPageUrl", - "healthChecks" - ] - } - ] + "healthCheckUrl": { + "type": "string" + }, + "landingPageUrl": { + "type": "string" + }, + "healthChecks": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["HEALTHY", "UNHEALTHY"] + }, + "httpResponseCode": { + "type": "string" + }, + "date": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "status", + "httpResponseCode", + "date" + ] + } + ] + } + }, + "required": [ + "componentName", + "componentCategory", + "componentType", + "ssmKey", + "healthCheckUrl", + "landingPageUrl", + "healthChecks" + ] + } } }, "required": [