Skip to content

Commit 361f9a0

Browse files
Ensuring Metric Values are Cast to Float when they are Strings (#237)
* Casting metric values to a float; Adding a test for it as well; * Checking for str to convert to float; Adding custom exception; * Removing unused init param; * Renaming exception name; * Adjusting exception check to include ValueError; Updating Exception name; Adding a couple tests; * Adding a couple validation tests for MetricDataRange as well; * Fixing some linting issues I missed;
1 parent 586c70a commit 361f9a0

File tree

7 files changed

+114
-3
lines changed

7 files changed

+114
-3
lines changed

prometheus_api_client/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ class PrometheusApiClientException(Exception):
55
"""API client exception, raises when response status code != 200."""
66

77
pass
8+
9+
10+
class MetricValueConversionError(Exception):
11+
"""Raises when we find a metric that is a string where we fail to convert it to a float."""
12+
13+
pass

prometheus_api_client/metric.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import datetime
44
import pandas
55

6+
from prometheus_api_client.exceptions import MetricValueConversionError
7+
68
try:
79
import matplotlib.pyplot as plt
810
from pandas.plotting import register_matplotlib_converters
@@ -69,7 +71,16 @@ def __init__(self, metric, oldest_data_datetime=None):
6971

7072
# if it is a single value metric change key name
7173
if "value" in metric:
72-
metric["values"] = [metric["value"]]
74+
datestamp = metric["value"][0]
75+
metric_value = metric["value"][1]
76+
if isinstance(metric_value, str):
77+
try:
78+
metric_value = float(metric_value)
79+
except (TypeError, ValueError):
80+
raise MetricValueConversionError(
81+
"Converting string metric value to float failed."
82+
)
83+
metric["values"] = [[datestamp, metric_value]]
7384

7485
self.metric_values = pandas.DataFrame(metric["values"], columns=["ds", "y"]).apply(
7586
pandas.to_numeric, errors="raise"

prometheus_api_client/metric_range_df.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pandas._typing import Axes, Dtype
44
from typing import Optional, Sequence
55

6+
from prometheus_api_client.exceptions import MetricValueConversionError
7+
68

79
class MetricRangeDataFrame(DataFrame):
810
"""Subclass to format and represent Prometheus query response as pandas.DataFrame.
@@ -71,7 +73,15 @@ def __init__(
7173
"data must be a range vector. Expected range vector, got instant vector"
7274
)
7375
for t in v["values"]:
74-
row_data.append({**v["metric"], "timestamp": t[0], "value": t[1]})
76+
metric_value = t[1]
77+
if isinstance(metric_value, str):
78+
try:
79+
metric_value = float(metric_value)
80+
except (TypeError, ValueError):
81+
raise MetricValueConversionError(
82+
"Converting string metric value to float failed."
83+
)
84+
row_data.append({**v["metric"], "timestamp": t[0], "value": metric_value})
7585

7686
# init df normally now
7787
super(MetricRangeDataFrame, self).__init__(

prometheus_api_client/metric_snapshot_df.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pandas._typing import Axes, Dtype
44
from typing import Optional, Sequence
55

6+
from prometheus_api_client.exceptions import MetricValueConversionError
7+
68

79
class MetricSnapshotDataFrame(DataFrame):
810
"""Subclass to format and represent Prometheus query response as pandas.DataFrame.
@@ -91,4 +93,10 @@ def __init__(
9193
@staticmethod
9294
def _get_nth_ts_value_pair(i: dict, n: int):
9395
val = i["values"][n] if "values" in i else i["value"]
94-
return {"timestamp": val[0], "value": val[1]}
96+
value = val[1]
97+
if isinstance(value, str):
98+
try:
99+
value = float(value)
100+
except (TypeError, ValueError):
101+
raise MetricValueConversionError("Converting string metric value to float failed.")
102+
return {"timestamp": val[0], "value": value}

tests/test_metric.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Unit tests for Metrics Class."""
22
import unittest
33
import datetime
4+
5+
import pytest
6+
47
from prometheus_api_client import Metric
8+
from prometheus_api_client.exceptions import MetricValueConversionError
59
from .test_with_metrics import TestWithMetrics
610

711

@@ -96,6 +100,27 @@ def test_oldest_data_datetime_with_timedelta(self): # noqa D102
96100
expected_start_time, new_metric.start_time, "Incorrect Start time after addition"
97101
)
98102

103+
def test_init_valid_string_metric_value(self):
104+
"""Ensures metric values provided as strings are properly cast to a numeric value (in this case, a float)."""
105+
test_metric = Metric(
106+
metric={
107+
"metric": {"__name__": "test_metric", "fake": "data",},
108+
"value": [1627485628.789, "26.82068965517243"],
109+
}
110+
)
111+
112+
self.assertTrue(isinstance(test_metric, Metric))
113+
114+
def test_init_invalid_string_metric_value(self):
115+
"""Ensures metric values provided as strings are properly cast to a numeric value (in this case, a float)."""
116+
with pytest.raises(MetricValueConversionError):
117+
Metric(
118+
metric={
119+
"metric": {"__name__": "test_metric", "fake": "data",},
120+
"value": [1627485628.789, "26.8206896551724326.82068965517243"],
121+
}
122+
)
123+
99124

100125
if __name__ == "__main__":
101126
unittest.main()

tests/test_metric_range_df.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Unit Tests for MetricRangeDataFrame."""
22
import unittest
33
import pandas as pd
4+
import pytest
5+
46
from prometheus_api_client import MetricRangeDataFrame
7+
from prometheus_api_client.exceptions import MetricValueConversionError
58
from .test_with_metrics import TestWithMetrics
69

710

@@ -90,6 +93,27 @@ def test_init_single_metric(self):
9093
"incorrect dataframe shape when initialized with single json list",
9194
)
9295

96+
def test_init_invalid_string_value(self):
97+
"""Ensures metric values provided as concatenated strings are caught with a meaningful exception."""
98+
with pytest.raises(MetricValueConversionError):
99+
MetricRangeDataFrame(
100+
{
101+
"metric": {"__name__": "test_metric", "fake": "data",},
102+
"values": [[1627485628.789, "26.8206896551724326.82068965517243"]],
103+
}
104+
)
105+
106+
def test_init_valid_string_value(self):
107+
"""Ensures metric values provided as a string but are valid floats are processed properly."""
108+
results = MetricRangeDataFrame(
109+
{
110+
"metric": {"__name__": "test_metric", "fake": "data",},
111+
"values": [[1627485628.789, "26.82068965517243"]],
112+
}
113+
)
114+
115+
self.assertEqual((1, 3), results.shape)
116+
93117

94118
if __name__ == "__main__":
95119
unittest.main()

tests/test_metric_snapshot_df.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
import unittest
33
import json
44
import os
5+
6+
import pytest
7+
58
from prometheus_api_client import MetricSnapshotDataFrame
9+
from prometheus_api_client.exceptions import MetricValueConversionError
610
from pandas.api.types import is_datetime64_any_dtype as is_dtype_datetime
711

812

@@ -90,6 +94,29 @@ def test_init_single_metric(self):
9094
"incorrect dataframe shape when initialized with single json list",
9195
)
9296

97+
def test_init_multiple_metrics(self):
98+
"""Ensures metric values provided as strings are properly cast to a numeric value (in this case, a float)."""
99+
raw_data = [
100+
{"metric": {"fake": "data",}, "value": [1627485628.789, "26.82068965517243"],},
101+
{"metric": {"fake": "data",}, "value": [1627485628.789, "26.82068965517243"],},
102+
]
103+
104+
test_df = MetricSnapshotDataFrame(data=raw_data)
105+
106+
self.assertTrue(isinstance(test_df["value"][0], float))
107+
108+
def test_init_invalid_float_error(self):
109+
"""Ensures metric values provided as strings are properly cast to a numeric value (in this case, a float)."""
110+
raw_data = [
111+
{
112+
"metric": {"fake": "data",},
113+
"value": [1627485628.789, "26.8206896551724326.82068965517243"],
114+
},
115+
]
116+
117+
with pytest.raises(MetricValueConversionError):
118+
MetricSnapshotDataFrame(data=raw_data)
119+
93120

94121
if __name__ == "__main__":
95122
unittest.main()

0 commit comments

Comments
 (0)