Error code as per specs 48/14248/1
authorSwaraj Kumar <swaraj.kumar@samsung.com>
Wed, 12 Mar 2025 07:47:36 +0000 (13:17 +0530)
committerSwaraj Kumar <swaraj.kumar@samsung.com>
Wed, 12 Mar 2025 07:47:36 +0000 (13:17 +0530)
Change-Id: I3e9c8ed24d814e507f7ae56a71873b43219e8d27
Signed-off-by: Swaraj Kumar <swaraj.kumar@samsung.com>
tests/test_trainingjob_controller.py
trainingmgr/controller/trainingjob_controller.py
trainingmgr/schemas/problemdetail_schema.py [new file with mode: 0644]

index 80545a3..b081096 100644 (file)
@@ -37,6 +37,7 @@ from trainingmgr.common.trainingmgr_config import TrainingMgrConfig
 from trainingmgr.common.exceptions_utls import DBException, TMException
 from trainingmgr.models import TrainingJob
 from trainingmgr.models import FeatureGroup
+from trainingmgr.schemas.problemdetail_schema import ProblemDetails
 from trainingmgr.common.trainingConfig_parser import getField
 
 #mock ModelMetricsSdk before importing
@@ -50,208 +51,194 @@ trainingmgr_main.LOCK = Lock()
 trainingmgr_main.DATAEXTRACTION_JOBS_CACHE = {}
 
 
-class Test_create_trainingjob:
+class TestCreateTrainingJob:
     def setup_method(self):
         app = Flask(__name__)
         app.register_blueprint(training_job_controller)
         self.client = app.test_client()
-
-    mocked_TRAININGMGR_CONFIG_OBJ = mock.Mock(name="TRAININGMGR_CONFIG_OBJ")
-    attrs_TRAININGMGR_CONFIG_OBJ = {'kf_adapter_ip.return_value': '123', 'kf_adapter_port.return_value': '100'}
-    mocked_TRAININGMGR_CONFIG_OBJ.configure_mock(**attrs_TRAININGMGR_CONFIG_OBJ)
-
     def test_create_trainingjob_missing_training_config(self):
         trainingmgr_main.LOGGER.debug("******* test_create_trainingjob_missing_training_config *******")
-        expected_data = "The training_config is missing"
+        expected_data = {
+            "title": "Bad Request",
+            "status": 400,
+            "detail": "The 'training_config' field is missing."
+        }
         trainingjob_req = {
-                    "modelId":{
-                        "modelname": "modeltest",
-                        "modelversion": "1"
-                    }
+            "modelId": {
+                "modelname": "modeltest",
+                "modelversion": "1"
             }
-        response = self.client.post("/training-jobs", data = json.dumps(trainingjob_req),
+        }
+        response = self.client.post("/training-jobs", data=json.dumps(trainingjob_req),
                                     content_type="application/json")
         trainingmgr_main.LOGGER.debug(response.data)
-        print(response)
-        assert response.status_code == status.HTTP_400_BAD_REQUEST
-        assert expected_data in str(response.data)
-
+        assert response.status_code == 400
+        assert response.json == expected_data
     def test_create_trainingjob_invalid_training_config(self):
         trainingmgr_main.LOGGER.debug("******* test_create_trainingjob_invalid_training_config *******")
-        expected_data = "The TrainingConfig is not correct"
+        expected_data = {
+            "title": "Bad Request",
+            "status": 400,
+            "detail": "The provided 'training_config' is not valid."
+        }
         trainingjob_req = {
-                    "modelId":{
-                        "modelname": "modeltest",
-                        "modelversion": "1"
-                    },
-                    "training_config": {
-                        "description": "trainingjob for testing"
-                    }
+            "modelId": {
+                "modelname": "modeltest",
+                "modelversion": "1"
+            },
+            "training_config": {
+                "description": "training job for testing"
             }
+        }
         response = self.client.post("/training-jobs", data=json.dumps(trainingjob_req),
                                     content_type="application/json")
         trainingmgr_main.LOGGER.debug(response.data)
-        assert response.status_code == status.HTTP_400_BAD_REQUEST
-        assert expected_data in str(response.data)
-
-    @patch('trainingmgr.controller.trainingjob_controller.get_modelinfo_by_modelId_service', return_value = None)
+        assert response.status_code == 400
+        assert response.json == expected_data
+    @patch('trainingmgr.controller.trainingjob_controller.get_modelinfo_by_modelId_service', return_value=None)
     def test_create_trainingjob_model_not_registered(self, mock1):
         trainingmgr_main.LOGGER.debug("******* test_create_trainingjob_model_not_registered *******")
-        expected_data = "Model name = test_model and Model version = 1 is not registered at MME, Please first register at MME and then continue"
+        expected_data = {
+            "title": "Bad Request",
+            "status": 400,
+            "detail": "Model 'test_model' version '1' is not registered at MME. Please register at MME first."
+        }
         trainingjob_req = {
-            "modelId":{
-                "modelname": "test_model", 
+            "modelId": {
+                "modelname": "test_model",
                 "modelversion": "1"
             },
             "model_location": "",
             "training_config": {
-                    "description": "trainingjob for testing",
-                    "dataPipeline": {
-                        "feature_group_name": "testing_influxdb_01",
-                        "query_filter": "",
-                        "arguments": "{'epochs': 1'}"
-                    },
-                    "trainingPipeline": {
-                        "training_pipeline_name": "qoe_Pipeline",
-                        "training_pipeline_version": "qoe_Pipeline",
-                        "retraining_pipeline_name": "qoe_PipelineRetrain",
-                        "retraining_pipeline_version": "qoe_PipelineRetrain",
-                    }
-            },
+                "description": "trainingjob for testing",
+                "dataPipeline": {
+                    "feature_group_name": "testing_influxdb_03",
+                    "query_filter": "",
+                    "arguments": {"epochs": 10}
+                },
+                "trainingPipeline": {
+                        "training_pipeline_name": "qoe_Pipeline", 
+                        "training_pipeline_version": "qoe_Pipeline", 
+                        "retraining_pipeline_name":"qoe_Pipeline_retrain",
+                        "retraining_pipeline_version":"qoe_Pipeline_retrain"
+                }
+            }
         }
-        response = self.client.post("/training-jobs", data=json.dumps(trainingjob_req), content_type="application/json")
-        trainingmgr_main.LOGGER.debug(response.data)
-        print(response.data)
-        assert response.status_code == status.HTTP_400_BAD_REQUEST
-        assert expected_data in str(response.data)
+        response = self.client.post("/training-jobs", data=json.dumps(trainingjob_req),
+                                    content_type="application/json")
+        print("Actual Response:", response.json)  # Debugging
+        assert response.status_code == 400
+        assert response.json == expected_data
 
     registered_model_list = [{"modelLocation": "s3://different-location"}]
     @patch('trainingmgr.controller.trainingjob_controller.get_modelinfo_by_modelId_service', return_value=registered_model_list)
     def test_create_trainingjob_model_location_mismatch(self, mock1):
-        trainingmgr_main.LOGGER.debug("******* test_create_trainingjob_model_location_mismatch *******")
-        expected_data = "Model name = test_model and Model version = 1 and trainingjob created does not have same modelLocation, Please first register at MME properly and then continue"
+        expected_data = {
+            "title": "Bad Request",
+            "status": 400,
+            "detail": "Model 'test_model' version '1' does not match the registered model location."
+        }
         trainingjob_req = {
-            "modelId":{
-                "modelname": "test_model", 
+            "modelId": {
+                "modelname": "test_model",
                 "modelversion": "1"
             },
-            "model_location": "s3://model-location",
+            "model_location": "",
             "training_config": {
-                    "description": "trainingjob for testing",
-                    "dataPipeline": {
-                        "feature_group_name": "testing_influxdb_01",
-                        "query_filter": "",
-                        "arguments": "{'epochs': 1'}"
-                    },
-                    "trainingPipeline": {
-                        "training_pipeline_name": "qoe_Pipeline",
-                        "training_pipeline_version": "qoe_Pipeline",
-                        "retraining_pipeline_name": "qoe_PipelineRetrain",
-                        "retraining_pipeline_version": "qoe_PipelineRetrain",
-                    }
-            },
+                "description": "trainingjob for testing",
+                "dataPipeline": {
+                    "feature_group_name": "testing_influxdb_03",
+                    "query_filter": "",
+                    "arguments": {"epochs": 10}
+                },
+                "trainingPipeline": {
+                        "training_pipeline_name": "qoe_Pipeline", 
+                        "training_pipeline_version": "qoe_Pipeline", 
+                        "retraining_pipeline_name":"qoe_Pipeline_retrain",
+                        "retraining_pipeline_version":"qoe_Pipeline_retrain"
+                }
+            }
         }
-        response = self.client.post("/training-jobs", data=json.dumps(trainingjob_req), content_type="application/json")
-        print(response.data)
-        trainingmgr_main.LOGGER.debug(response.data)
-        assert response.status_code == status.HTTP_400_BAD_REQUEST
-        assert expected_data in str(response.data)
-
-class Test_DeleteTrainingJob:
+        response = self.client.post("/training-jobs", data=json.dumps(trainingjob_req),
+                                    content_type="application/json")
+        assert response.status_code == 400
+        assert response.json == expected_data
+        
+class TestDeleteTrainingJob:
     def setup_method(self):
         app = Flask(__name__)
         app.register_blueprint(training_job_controller)
         self.client = app.test_client()
-
     @patch('trainingmgr.controller.trainingjob_controller.delete_training_job', return_value=True)
     def test_delete_trainingjob_success(self, mock1):
-        response = self.client.delete("/training-jobs/{}".format("123"))
-        trainingmgr_main.LOGGER.debug(response.data)
-        assert response.status_code == status.HTTP_204_NO_CONTENT
-    
+        response = self.client.delete("/training-jobs/123")
+        assert response.status_code == 204
+        assert response.data == b''
     @patch('trainingmgr.controller.trainingjob_controller.delete_training_job', return_value=False)
     def test_delete_trainingjob_not_found(self, mock1):
-        expected_data = {'message': 'training job with given id is not found'}
-        
-        response = self.client.delete("/training-jobs/{}".format("123"))
-        trainingmgr_main.LOGGER.debug(response.data)
-        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
-        assert expected_data == response.json
-
+        expected_data = {
+            "title": "Not Found",
+            "status": 404,
+            "detail": "Training job with ID 123 does not exist."
+        }
+        response = self.client.delete("/training-jobs/123")
+        assert response.status_code == 404
+        assert response.json == expected_data
 
-class Test_GetTrainingJobs:
+class TestGetTrainingJobs:
     def setup_method(self):
         app = Flask(__name__)
         app.register_blueprint(training_job_controller)
         self.client = app.test_client()
-    
-    tjs = [{'id': 1, 'name': 'Test Job'}]
-    @patch('trainingmgr.controller.trainingjob_controller.get_trainining_jobs', return_value = tjs)
-    @patch('trainingmgr.controller.trainingjob_controller.trainingjobs_schema.dump', return_value = tjs)
+    @patch('trainingmgr.controller.trainingjob_controller.get_trainining_jobs', return_value=[{"id": 1, "name": "Test Job"}])
+    @patch('trainingmgr.controller.trainingjob_controller.trainingjobs_schema.dump', return_value=[{"id": 1, "name": "Test Job"}])
     def test_get_trainingjobs_success(self, mock1, mock2):
-        expected_data = [{"id": 1, "name": "Test Job"}]
         response = self.client.get('/training-jobs/')
         assert response.status_code == 200
-        assert expected_data == response.json
-
+        assert response.json == [{"id": 1, "name": "Test Job"}]
     @patch('trainingmgr.controller.trainingjob_controller.get_trainining_jobs')
     def test_get_trainingjobs_tmexception(self, mock_get_trainingjobs):
-        mock_get_trainingjobs.side_effect = TMException('Training jobs not found')
-
-        response = self.client.get('/training-jobs/')
-        assert response.status_code == 400
-        assert response.json['message'] == 'Training jobs not found'
-
-    @patch('trainingmgr.controller.trainingjob_controller.get_trainining_jobs')
-    def test_get_trainingjobs_generic_exception(self, mock_get_trainingjobs):
-        mock_get_trainingjobs.side_effect = Exception('Unexpected error')
+        mock_get_trainingjobs.side_effect = Exception('Training jobs not found')
+        expected_data = {
+            "title": "Internal Server Error",
+            "status": 500,
+            "detail": "Training jobs not found"
+        }
         response = self.client.get('/training-jobs/')
         assert response.status_code == 500
-        assert response.json['message'] == 'Unexpected error'
+        assert response.json == expected_data
 
-class Test_GetTrainingJob:
+class TestGetTrainingJob:
     def setup_method(self):
         app = Flask(__name__)
         app.register_blueprint(training_job_controller)
         self.client = app.test_client()
-
-    tj = {'id': 1, 'name': 'Test Job'}
-    @patch('trainingmgr.controller.trainingjob_controller.get_training_job', return_value = tj)
-    @patch('trainingmgr.controller.trainingjob_controller.trainingjob_schema.dump', return_value = tj)
+    @patch('trainingmgr.controller.trainingjob_controller.get_training_job', return_value={"id": 1, "name": "Test Job"})
+    @patch('trainingmgr.controller.trainingjob_controller.trainingjob_schema.dump', return_value={"id": 1, "name": "Test Job"})
     def test_get_trainingjob_success(self, mock_schema_dump, mock_get_training_job):
         response = self.client.get('/training-jobs/1')
         assert response.status_code == 200
-        assert response.json == {'id': 1, 'name': 'Test Job'}
-
-    @patch('trainingmgr.controller.trainingjob_controller.get_training_job')
-    def test_get_trainingjob_tmexception(self, mock_get_training_job):
-        # Simulate TMException
-        mock_get_training_job.side_effect = TMException('Training job not found')
-
-        response = self.client.get('/training-jobs/1')
-        assert response.status_code == 400
-        assert response.json['message'] == 'Training job not found'
-
+        assert response.json == {"id": 1, "name": "Test Job"}
     @patch('trainingmgr.controller.trainingjob_controller.get_training_job')
     def test_get_trainingjob_generic_exception(self, mock_get_training_job):
         mock_get_training_job.side_effect = Exception('Unexpected error')
+        expected_data = {
+            "title": "Internal Server Error",
+            "status": 500,
+            "detail": "Unexpected error"
+        }
         response = self.client.get('/training-jobs/1')
         assert response.status_code == 500
-        assert response.json['message'] == 'Unexpected error'
+        assert response.json == expected_data
 
-
-class Test_GetTrainingJobStatus:
+class TestGetTrainingJobStatus:
     def setup_method(self):
         app = Flask(__name__)
         app.register_blueprint(training_job_controller)
         self.client = app.test_client()
-
     expected_data = {"status": "running"}
     @patch('trainingmgr.controller.trainingjob_controller.get_steps_state', return_value=json.dumps(expected_data))
     def test_get_trainingjob_status(self, mock1):
-        expected_data = {"status": "running"}
-        response = self.client.get("/training-jobs/{}/status".format("123"))
-        trainingmgr_main.LOGGER.debug(response.data)
-        assert response.status_code == status.HTTP_200_OK
-        assert expected_data == response.json
-
+        response = self.client.get("/training-jobs/123/status")
+        assert response.status_code == 200
+        assert response.json == {"status": "running"}
\ No newline at end of file
index 1676277..d618f8e 100644 (file)
@@ -24,11 +24,14 @@ from marshmallow import ValidationError
 from trainingmgr.common.exceptions_utls import TMException
 from trainingmgr.common.trainingmgr_config import TrainingMgrConfig
 from trainingmgr.schemas.trainingjob_schema import TrainingJobSchema
+from trainingmgr.schemas.problemdetail_schema import ProblemDetails
 from trainingmgr.service.training_job_service import delete_training_job, create_training_job, get_training_job, get_trainining_jobs, \
 get_steps_state
 from trainingmgr.common.trainingmgr_util import check_key_in_dictionary
 from trainingmgr.common.trainingConfig_parser import validateTrainingConfig
 from trainingmgr.service.mme_service import get_modelinfo_by_modelId_service
+  
+
 training_job_controller = Blueprint('training_job_controller', __name__)
 LOGGER = TrainingMgrConfig().logger
 TRAININGMGR_CONFIG_OBJ = TrainingMgrConfig()
@@ -40,96 +43,74 @@ MIMETYPE_JSON = "application/json"
 
 @training_job_controller.route('/training-jobs/<int:training_job_id>', methods=['DELETE'])
 def delete_trainingjob(training_job_id):
-    LOGGER.debug(f'delete training job : {training_job_id}')
+    LOGGER.debug(f'Delete training job : {training_job_id}')
     try:
         if delete_training_job(int(training_job_id)):
-            LOGGER.debug(f'training job with {training_job_id} is deleted successfully.')
+            LOGGER.debug(f'Training job {training_job_id} deleted successfully.')
             return '', 204
         else:
-            LOGGER.debug(f'training job with {training_job_id} does not exist.')
-            return jsonify({
-                'message': 'training job with given id is not found'
-            }), 500 
-         
+            LOGGER.debug(f'Training job {training_job_id} not found.')
+            return ProblemDetails(404, "Not Found", f"Training job with ID {training_job_id} does not exist.").to_json()
     except Exception as e:
-        return jsonify({
-            'message': str(e)
-        }), 500
-    
-    
+        LOGGER.error(f"Error deleting training job {training_job_id}: {str(e)}")
+        return ProblemDetails(500, "Internal Server Error", str(e)).to_json()
+
 @training_job_controller.route('/training-jobs', methods=['POST'])
 def create_trainingjob():
-
     try:
-        LOGGER.debug(f"request for training with json {request.get_json()}")
+        LOGGER.debug(f"Request for training job with JSON: {request.get_json()}")
         request_json = request.get_json()
-
-        if check_key_in_dictionary(["training_config"], request_json):
-            request_json['training_config'] = json.dumps(request_json["training_config"])
-        else:
-            return jsonify({'Exception': 'The training_config is missing'}), status.HTTP_400_BAD_REQUEST
-        
+        if not check_key_in_dictionary(["training_config"], request_json):
+            return ProblemDetails(400, "Bad Request", "The 'training_config' field is missing.").to_json()
+        request_json['training_config'] = json.dumps(request_json["training_config"])
         trainingjob = trainingjob_schema.load(request_json)
-
         trainingConfig = trainingjob.training_config
-        if(not validateTrainingConfig(trainingConfig)):
-            return jsonify({'Exception': 'The TrainingConfig is not correct'}), status.HTTP_400_BAD_REQUEST
-        
+        if not validateTrainingConfig(trainingConfig):
+            return ProblemDetails(400, "Bad Request", "The provided 'training_config' is not valid.").to_json()
         model_id = trainingjob.modelId
         registered_model_list = get_modelinfo_by_modelId_service(model_id.modelname, model_id.modelversion)
-        # Verify if the modelId is registered over mme or not
         if registered_model_list is None:
-            return jsonify({"Exception":f"Model name = {model_id.modelname} and Model version = {model_id.modelversion} is not registered at MME, Please first register at MME and then continue"}), status.HTTP_400_BAD_REQUEST
-
+            return ProblemDetails(400, "Bad Request", f"Model '{model_id.modelname}' version '{model_id.modelversion}' is not registered at MME. Please register at MME first.").to_json()
         registered_model_dict = registered_model_list[0]
         if registered_model_dict["modelLocation"] != trainingjob.model_location:
-            return jsonify({"Exception":f"Model name = {model_id.modelname} and Model version = {model_id.modelversion} and trainingjob created does not have same modelLocation, Please first register at MME properly and then continue"}), status.HTTP_400_BAD_REQUEST
-        
-        return create_training_job(trainingjob=trainingjob, registered_model_dict= registered_model_dict)
-        
+            return ProblemDetails(400, "Bad Request", f"Model '{model_id.modelname}' version '{model_id.modelversion}' does not match the registered model location.").to_json()
+        return create_training_job(trainingjob=trainingjob, registered_model_dict=registered_model_dict)
     except ValidationError as error:
-        return jsonify(error.messages), status.HTTP_400_BAD_REQUEST
+        return ProblemDetails(400, "Validation Error", str(error.messages)).to_json()
     except Exception as e:
-        return jsonify({
-            'message': str(e)
-        }), 500
+        LOGGER.error(f"Error creating training job: {str(e)}")
+        return ProblemDetails(500, "Internal Server Error", str(e)).to_json()
 
 @training_job_controller.route('/training-jobs/', methods=['GET'])
 def get_trainingjobs():
-    LOGGER.debug(f'get the trainingjobs')
+    LOGGER.debug(f'Fetching all training jobs')
     try:
         resp = trainingjobs_schema.dump(get_trainining_jobs())
         return jsonify(resp), 200
     except TMException as err:
-        return jsonify({
-            'message': str(err)
-        }), 400
+        return ProblemDetails(400, "Bad Request", str(err)).to_json()
     except Exception as e:
-        return jsonify({
-            'message': str(e)
-        }), 500
+        LOGGER.error(f"Error fetching training jobs: {str(e)}")
+        return ProblemDetails(500, "Internal Server Error", str(e)).to_json()
 
 @training_job_controller.route('/training-jobs/<int:training_job_id>', methods=['GET'])
 def get_trainingjob(training_job_id):
-    LOGGER.debug(f'get the trainingjob correspoinding to id: {training_job_id}')
+    LOGGER.debug(f'Fetching training job {training_job_id}')
     try:
         return jsonify(trainingjob_schema.dump(get_training_job(training_job_id))), 200
     except TMException as err:
-        return jsonify({
-            'message': str(err)
-        }), 400
+        return ProblemDetails(400, "Bad Request", str(err)).to_json()
     except Exception as e:
-        return jsonify({
-            'message': str(e)
-        }), 500
+        LOGGER.error(f"Error fetching training job {training_job_id}: {str(e)}")
+        return ProblemDetails(500, "Internal Server Error", str(e)).to_json()
+
 
 @training_job_controller.route('/training-jobs/<int:training_job_id>/status', methods=['GET'])
 def get_trainingjob_status(training_job_id):
-    LOGGER.debug(f'request to get the training_job status of {training_job_id}')
+    LOGGER.debug(f'Requesting status for training job {training_job_id}')
     try:
         status = get_steps_state(training_job_id)
         return jsonify(json.loads(status)), 200
     except Exception as err:
-        return jsonify({
-            'message': str(err)
-        }), 500
+        LOGGER.error(f"Error fetching status for training job {training_job_id}: {str(err)}")
+        return ProblemDetails(500, "Internal Server Error", str(err)).to_json()
diff --git a/trainingmgr/schemas/problemdetail_schema.py b/trainingmgr/schemas/problemdetail_schema.py
new file mode 100644 (file)
index 0000000..0453157
--- /dev/null
@@ -0,0 +1,30 @@
+from flask import jsonify
+
+class ProblemDetails:
+    """
+    A structured class for generating error responses in OpenAPI Problem Details format.
+    """
+    def __init__(self, status: int, title: str, detail: str):
+        """
+        Initialize a ProblemDetails instance.
+        :param status: HTTP status code (e.g., 400, 404, 500)
+        :param title: Short description of the error
+        :param detail: Detailed error message
+        """
+        self.status = status
+        self.title = title
+        self.detail = detail
+    def to_dict(self):
+        """
+        Convert the ProblemDetails object into a dictionary.
+        """
+        return {
+            "title": self.title,
+            "status": self.status,
+            "detail": self.detail
+        }
+    def to_json(self):
+        """
+        Convert the ProblemDetails object into a Flask JSON response.
+        """
+        return jsonify(self.to_dict()), self.status, {"Content-Type": "application/problem+json"}