From 822c1327df0f61c8ea5ed40ebb3f378631dde0c4 Mon Sep 17 00:00:00 2001 From: Denis Zhdanov Date: Sun, 17 Apr 2022 23:10:19 +0200 Subject: [PATCH] [1.1.x] raise exception when not recoverable | use exception subclass InputValidationError | use different handlers for exceptions | Implement upper lower case functions (#2586) (#2748) * raise exception when not recoverable (cherry picked from commit ae416e953772cdb475af2f722680b8218552b3cd) * use exception subclass InputValidationError (cherry picked from commit cc289928018582ce026001c8d5d1d1319dd45566) * use different handlers for exceptions (cherry picked from commit 798de47b7831a73f1922213a59805a7c17c4f249) * Implement upper lower case functions (#2586) * implement functions to upper/lower case * add tests * better way to update a single char within a string Co-Authored-By: Dan Cech * add assert for linter Co-authored-by: Dan Cech (cherry picked from commit 8e99587a8a2f7dc3bd6720cef17ba3afbf6cf085) Co-authored-by: replay --- webapp/graphite/render/evaluator.py | 27 +++--- webapp/graphite/render/functions.py | 70 +++++++++++++++ webapp/tests/test_functions.py | 128 ++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 16 deletions(-) diff --git a/webapp/graphite/render/evaluator.py b/webapp/graphite/render/evaluator.py index 12179dcc4..f502c1f1d 100644 --- a/webapp/graphite/render/evaluator.py +++ b/webapp/graphite/render/evaluator.py @@ -104,32 +104,27 @@ def evaluateTokens(requestContext, tokens, replacements=None, pipedArg=None): kwargs = dict([(kwarg.argname, evaluateTokens(requestContext, kwarg.args[0], replacements)) for kwarg in tokens.call.kwargs]) - def handleInvalidParameters(e): - e.setSourceIdHeaders(requestContext.get('sourceIdHeaders', {})) - e.setTargets(requestContext.get('targets', [])) - e.setFunction(tokens.call.funcname, args, kwargs) - - if settings.ENFORCE_INPUT_VALIDATION: - raise e - - if not getattr(handleInvalidParameters, 'alreadyLogged', False): - log.warning('%s', str(e)) - - # only log invalid parameters once - setattr(handleInvalidParameters, 'alreadyLogged', True) - if hasattr(func, 'params'): try: (args, kwargs) = validateParams(tokens.call.funcname, func.params, args, kwargs) except InputParameterError as e: - handleInvalidParameters(e) + e.setSourceIdHeaders(requestContext.get('sourceIdHeaders', {})) + e.setTargets(requestContext.get('targets', [])) + e.setFunction(tokens.call.funcname, args, kwargs) + if settings.ENFORCE_INPUT_VALIDATION: + raise + else: + log.warning('Validation Error: %s', str(e)) try: return func(requestContext, *args, **kwargs) except NormalizeEmptyResultError: return [] except InputParameterError as e: - handleInvalidParameters(e) + e.setSourceIdHeaders(requestContext.get('sourceIdHeaders', {})) + e.setTargets(requestContext.get('targets', [])) + e.setFunction(tokens.call.funcname, args, kwargs) + raise return evaluateScalarTokens(tokens) diff --git a/webapp/graphite/render/functions.py b/webapp/graphite/render/functions.py index 9514b35c7..14794aac8 100644 --- a/webapp/graphite/render/functions.py +++ b/webapp/graphite/render/functions.py @@ -2830,6 +2830,74 @@ def legendValue(requestContext, seriesList, *valueTypes): ] +def toUpperCase(requestContext, seriesList, *pos): + """ + Takes one metric or a wildcard seriesList and uppers the case of each letter. + + Optionally, a letter position to upper case can be specified, in which case + only the letter at the specified position gets upper-cased. + The position parameter may be given multiple times. + The position parameter may be negative to define a position relative to the + end of the metric name. + """ + + for series in seriesList: + if len(pos) == 0: + series.name = series.name.upper() + else: + tmpName = list(series.name) + for i in pos: + assert isinstance(i, int) + try: + tmpName[i] = tmpName[i].upper() + except IndexError: + pass + series.name = "".join(tmpName) + + return seriesList + + +toUpperCase.group = 'Alias' +toUpperCase.params = [ + Param('seriesList', ParamTypes.seriesList, required=True), + Param('pos', ParamTypes.integer, multiple=True) +] + + +def toLowerCase(requestContext, seriesList, *pos): + """ + Takes one metric or a wildcard seriesList and lowers the case of each letter. + + Optionally, a letter position to lower case can be specified, in which case + only the letter at the specified position gets lower-cased. + The position parameter may be given multiple times. + The position parameter may be negative to define a position relative to the + end of the metric name. + """ + + for series in seriesList: + if len(pos) == 0: + series.name = series.name.lower() + else: + tmpName = list(series.name) + for i in pos: + assert isinstance(i, int) + try: + tmpName[i] = tmpName[i].lower() + except IndexError: + pass + series.name = "".join(tmpName) + + return seriesList + + +toLowerCase.group = 'Alias' +toLowerCase.params = [ + Param('seriesList', ParamTypes.seriesList, required=True), + Param('pos', ParamTypes.integer, multiple=True) +] + + def alpha(requestContext, seriesList, alpha): """ Assigns the given alpha transparency setting to the series. Takes a float value between 0 and 1. @@ -6043,6 +6111,8 @@ def pieMinimum(requestContext, series): 'aliasQuery': aliasQuery, 'aliasSub': aliasSub, 'legendValue': legendValue, + 'upper': toUpperCase, + 'lower': toLowerCase, # Graph functions 'alpha': alpha, diff --git a/webapp/tests/test_functions.py b/webapp/tests/test_functions.py index 0f8cf3b85..cc92475ca 100644 --- a/webapp/tests/test_functions.py +++ b/webapp/tests/test_functions.py @@ -4318,6 +4318,134 @@ def test_legendValue_invalid_none(self): result = functions.legendValue({}, seriesList, "avg", "bogus") self.assertEqual(result, expectedResult) + def test_upper_without_positions(self): + seriesList = self._gen_series_list_with_data( + key=['', 'a', 'normal.metric.name.with.normal.length', '!@#$%^&*()_+'], + start=0, + end=4, + data=[[], [], [], []], + ) + expectedResult = [ + TimeSeries('', 0, 4, 1, []), + TimeSeries('A', 0, 4, 1, []), + TimeSeries('NORMAL.METRIC.NAME.WITH.NORMAL.LENGTH', 0, 4, 1, []), + TimeSeries('!@#$%^&*()_+', 0, 4, 1, []), + ] + result = functions.toUpperCase({}, seriesList) + self.assertEqual(result, expectedResult) + + def test_upper_with_one_position(self): + seriesList = self._gen_series_list_with_data( + key=[ + '', + 'normal.metric.name.with.normal.length', + ], + start=0, + end=4, + data=[[], [], [], []], + ) + + expectedResult = [ + TimeSeries('', 0, 4, 1, []), + TimeSeries('normal.metRic.name.with.normal.length', 0, 4, 1, []), + ] + seriesList = functions.toUpperCase({}, seriesList, 10) + self.assertEqual(seriesList, expectedResult) + + expectedResult[1].name = 'Normal.metRic.name.with.normal.length' + seriesList = functions.toUpperCase({}, seriesList, 0) + self.assertEqual(seriesList, expectedResult) + + expectedResult[1].name = 'Normal.metRic.name.with.normal.lengtH' + seriesList = functions.toUpperCase({}, seriesList, -1) + self.assertEqual(seriesList, expectedResult) + + expectedResult[1].name = 'Normal.metRic.name.with.normal.lengtH' + seriesList = functions.toUpperCase({}, seriesList, 1000) + self.assertEqual(seriesList, expectedResult) + + def test_upper_with_multiple_positions(self): + seriesList = self._gen_series_list_with_data( + key=[ + '', + 'normal.metric.name.with.normal.length', + ], + start=0, + end=4, + data=[[], [], [], []], + ) + + expectedResult = [ + TimeSeries('', 0, 4, 1, []), + TimeSeries('Normal.metric.nAme.with.normal.lenGtH', 0, 4, 1, []), + ] + seriesList = functions.toUpperCase({}, seriesList, 0, 15, 100, -3, -1, -1000) + self.assertEqual(seriesList, expectedResult) + + def test_lower_without_positions(self): + seriesList = self._gen_series_list_with_data( + key=['', 'A', 'NORMAL.METRIC.NAME.WITH.NORMAL.LENGTH', '!@#$%^&*()_+'], + start=0, + end=4, + data=[[], [], [], []], + ) + expectedResult = [ + TimeSeries('', 0, 4, 1, []), + TimeSeries('a', 0, 4, 1, []), + TimeSeries('normal.metric.name.with.normal.length', 0, 4, 1, []), + TimeSeries('!@#$%^&*()_+', 0, 4, 1, []), + ] + result = functions.toLowerCase({}, seriesList) + self.assertEqual(result, expectedResult) + + def test_lower_with_one_position(self): + seriesList = self._gen_series_list_with_data( + key=[ + '', + 'NORMAL.METRIC.NAME.WITH.NORMAL.LENGTH', + ], + start=0, + end=4, + data=[[], [], [], []], + ) + + expectedResult = [ + TimeSeries('', 0, 4, 1, []), + TimeSeries('NORMAL.METrIC.NAME.WITH.NORMAL.LENGTH', 0, 4, 1, []), + ] + seriesList = functions.toLowerCase({}, seriesList, 10) + self.assertEqual(seriesList, expectedResult) + + expectedResult[1].name = 'nORMAL.METrIC.NAME.WITH.NORMAL.LENGTH' + seriesList = functions.toLowerCase({}, seriesList, 0) + self.assertEqual(seriesList, expectedResult) + + expectedResult[1].name = 'nORMAL.METrIC.NAME.WITH.NORMAL.LENGTh' + seriesList = functions.toLowerCase({}, seriesList, -1) + self.assertEqual(seriesList, expectedResult) + + expectedResult[1].name = 'nORMAL.METrIC.NAME.WITH.NORMAL.LENGTh' + seriesList = functions.toLowerCase({}, seriesList, 1000) + self.assertEqual(seriesList, expectedResult) + + def test_lower_with_multiple_positions(self): + seriesList = self._gen_series_list_with_data( + key=[ + '', + 'NORMAL.METRIC.NAME.WITH.NORMAL.LENGTH', + ], + start=0, + end=4, + data=[[], [], [], []], + ) + + expectedResult = [ + TimeSeries('', 0, 4, 1, []), + TimeSeries('nORMAL.METRIC.NaME.WITH.NORMAL.LENgTh', 0, 4, 1, []), + ] + seriesList = functions.toLowerCase({}, seriesList, 0, 15, 100, -3, -1, -1000) + self.assertEqual(seriesList, expectedResult) + @patch('graphite.render.evaluator.prefetchData', lambda *_: None) def test_linearRegression(self): seriesList = self._gen_series_list_with_data(