Repository: bigfishgames-opensource/GameBenchAPI-PyClient (language: python)

Issue:
Refactor the FpsStability model to use the 'to_dataframe' utility method
As an engineer, I want the 'FpsStability' model to use the 'to_dataframe' utility method so that the data can be given to the user as a DataFrame.  

We will know this is complete when the 'FpsStability' model goes through the 'to_dataframe' utility method so that the data is able to be retrieved by the user.

Currently the model is trying to use the 'json_to_normalized_dataframe' utility method.  The response information for the 'FpsStability' model is different from all of the other time series data, as it has no 'samples' key.  The response JSON is a histogram showing the percent of time spent at a given frame rate.   This key is used by the 'json_to_normalized_dataframe' utility to normalize the data.  The fix should be to put the 'FpsStability' response data through the 'to_dataframe' utility.


Discussion / hints:

Fix patch:
diff --git a/gamebench_api_client/models/creator/model_creator.py b/gamebench_api_client/models/creator/model_creator.py
index a1a1620..7341046 100755
--- a/gamebench_api_client/models/creator/model_creator.py
+++ b/gamebench_api_client/models/creator/model_creator.py
@@ -9,12 +9,12 @@ class ModelCreator:
 
     MODELS_AND_MODULES = {
         "gamebench_api_client.models.dataframes.generic.generic_models":
-            ["Keyword", "Markers", "SessionNotes", "SessionSummary"],
+            ["FpsStability", "Keyword", "Markers", "SessionNotes", "SessionSummary"],
         "gamebench_api_client.models.dataframes.session_detail.session_detail_models":
             ["App", "Device", "Location", "Metrics", "NetworkUsage"],
         "gamebench_api_client.models.dataframes.time_series.time_series_models":
             [
-                "Battery", "Cpu", "CpuCoreFrequency", "Energy", "Fps", "FpsStability",
+                "Battery", "Cpu", "CpuCoreFrequency", "Energy", "Fps",
                 "Energy", "GpuImg", "Gpu", "Janks", "Memory", "Network", "Power"
             ]
     }
diff --git a/gamebench_api_client/models/dataframes/generic/generic_models.py b/gamebench_api_client/models/dataframes/generic/generic_models.py
index 776e7a4..b2dedf0 100755
--- a/gamebench_api_client/models/dataframes/generic/generic_models.py
+++ b/gamebench_api_client/models/dataframes/generic/generic_models.py
@@ -8,6 +8,12 @@
 from gamebench_api_client.models.dataframes.generic.abstract_generic import AbstractGenericModel
 
 
+class FpsStability(AbstractGenericModel):
+    """ Object to set the FPS Stability information for a session."""
+
+    METRIC_PATH = '/fpsStability'
+
+
 class Keyword(AbstractGenericModel):
     """ Object to set the results for a keyword search."""
 
diff --git a/gamebench_api_client/models/dataframes/time_series/time_series_models.py b/gamebench_api_client/models/dataframes/time_series/time_series_models.py
index 8eddc87..62f1dce 100755
--- a/gamebench_api_client/models/dataframes/time_series/time_series_models.py
+++ b/gamebench_api_client/models/dataframes/time_series/time_series_models.py
@@ -32,12 +32,6 @@ class Energy(AbstractTimeSeriesModel):
     METRIC_PATH = '/energy'
 
 
-class FpsStability(AbstractTimeSeriesModel):
-    """ Object to set the FPS Stability information for a session."""
-
-    METRIC_PATH = '/fpsStability'
-
-
 class Fps(AbstractTimeSeriesModel):
     """ Object to set the FPS information for a session."""
 


Test patch:
diff --git a/tests/fixtures/constants.py b/tests/fixtures/constants.py
index 0e9321e..ea54f7e 100755
--- a/tests/fixtures/constants.py
+++ b/tests/fixtures/constants.py
@@ -38,6 +38,7 @@
 BASE_SESSION_URL = BASE_URL + VERSION + SESSION_SUFFIX + '/' + SESSION_ID
 DEFAULT_SESSION_URL = BASE_URL + VERSION + SESSION_SUFFIX + '/' + SESSION_ID + METRIC
 GENERIC_SESSION_URL = BASE_URL + VERSION + SESSION_SUFFIX + '/' + SESSION_ID + '/notes'
+FPS_STABILITY_URL = BASE_URL + VERSION + SESSION_SUFFIX + '/' + SESSION_ID + '/fpsStability'
 SESSION_DATA = {
     "data": {
         "test_data": "test_data"
@@ -158,6 +159,9 @@
         }
     }
 }
+FPS_STABILITY_PARAMS = {
+    'session_id': SESSION_ID
+}
 
 # Mock Paths
 SESSION_RESPONSE_RETRIEVER_PATH = 'gamebench_api_client.api.response.response_mediator.ResponseRetriever'
diff --git a/tests/test_integration/test_models/test_creator/test_model_creator.py b/tests/test_integration/test_models/test_creator/test_model_creator.py
index d29061c..b5b87fb 100755
--- a/tests/test_integration/test_models/test_creator/test_model_creator.py
+++ b/tests/test_integration/test_models/test_creator/test_model_creator.py
@@ -173,3 +173,38 @@ def test_filter_using_apps(self, mock_return):
         actual = model.data
 
         assert_frame_equal(actual, expected)
+
+    @requests_mock.Mocker()
+    def test_fps_stability(self, mock_return):
+        """ FPS Stability gets the correct data back
+
+            The FPS Stability needs to go through the 'to_dataframe' utility method.
+            Regression test for https://github.com/bigfishgames/gamebenchapi-pyclient/issues/67
+        """
+
+        with open(os.path.join(
+            PARENT_DIR + '/fixtures/' + "authentication_token.json")) as \
+            json_data:
+            self.auth_token = json.load(json_data)
+        with open(os.path.join(
+            PARENT_DIR + API_SAMPLES + "fps_stability.json")) as \
+            json_data:
+            self.fps_stability_json = json.load(json_data)
+        mock_return.request(
+            'POST',
+            AUTH_URL,
+            json=self.auth_token
+        )
+        mock_return.request(
+            'GET',
+            FPS_STABILITY_URL,
+            json=self.fps_stability_json['response']
+        )
+        creator = ModelCreator('FpsStability', **FPS_STABILITY_PARAMS)
+        model = creator.get_model()
+        expected = pandas.DataFrame(
+            [self.fps_stability_json['response']]
+        )
+        actual = model.data
+
+        assert_frame_equal(actual, expected)
diff --git a/tests/test_unit/test_models/test_dataframes/test_generic/test_generic_models.py b/tests/test_unit/test_models/test_dataframes/test_generic/test_generic_models.py
index 551e91e..0d42db1 100755
--- a/tests/test_unit/test_models/test_dataframes/test_generic/test_generic_models.py
+++ b/tests/test_unit/test_models/test_dataframes/test_generic/test_generic_models.py
@@ -13,6 +13,9 @@ def test_generic_models_create(self):
         with patch(f'{DATAFRAMES_PATH}.generic.generic_models.AbstractGenericModel.get_data'), \
                 patch(f'{ABSTRACT_GENERIC}.GenericMediator'), \
                 patch(f'{ABSTRACT_GENERIC}.Authenticator'):
+            with self.subTest():
+                fps_stability = FpsStability()
+                self.assertEqual('/fpsStability', fps_stability.request_parameters['metric'])
             with self.subTest():
                 keyword = Keyword()
                 self.assertEqual('/typeahead', keyword.request_parameters['metric'])
diff --git a/tests/test_unit/test_models/test_dataframes/test_time_series/test_time_series_models.py b/tests/test_unit/test_models/test_dataframes/test_time_series/test_time_series_models.py
index 5251df5..effb986 100755
--- a/tests/test_unit/test_models/test_dataframes/test_time_series/test_time_series_models.py
+++ b/tests/test_unit/test_models/test_dataframes/test_time_series/test_time_series_models.py
@@ -24,9 +24,6 @@ def test_time_series_models_create(self):
             with self.subTest():
                 energy = Energy()
                 self.assertEqual('/energy', energy.request_parameters['metric'])
-            with self.subTest():
-                fps_stability = FpsStability()
-                self.assertEqual('/fpsStability', fps_stability.request_parameters['metric'])
             with self.subTest():
                 fps = Fps()
                 self.assertEqual('/fps', fps.request_parameters['metric'])