diff --git a/kubernetes/test/test_quantity.py b/kubernetes/test/test_quantity.py new file mode 100644 index 0000000000..344f216aee --- /dev/null +++ b/kubernetes/test/test_quantity.py @@ -0,0 +1,71 @@ +# coding: utf-8 +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import unittest +from kubernetes.utils import parse_quantity + + +class TestQuantity(unittest.TestCase): + def test_parse(self): + self.assertEqual(parse_quantity(2), 2) + self.assertEqual(parse_quantity(2.), 2) + self.assertEqual(parse_quantity("123"), 123) + self.assertEqual(parse_quantity("2"), 2) + self.assertEqual(parse_quantity("2m"), 0.002) + self.assertEqual(parse_quantity("223k"), 223000) + self.assertEqual(parse_quantity("002M"), 2 * 1000**2) + self.assertEqual(parse_quantity("2M"), 2 * 1000**2) + self.assertEqual(parse_quantity("4123G"), 4123 * 1000**3) + self.assertEqual(parse_quantity("2T"), 2 * 1000**4) + self.assertEqual(parse_quantity("2P"), 2 * 1000**5) + self.assertEqual(parse_quantity("2E"), 2 * 1000**6) + + self.assertEqual(parse_quantity("2m"), 0.002) + self.assertEqual(parse_quantity("223Ki"), 223 * 1024) + self.assertEqual(parse_quantity("002Mi"), 2 * 1024**2) + self.assertEqual(parse_quantity("2Mi"), 2 * 1024**2) + self.assertEqual(parse_quantity("2Gi"), 2 * 1024**3) + self.assertEqual(parse_quantity("4123Gi"), 4123 * 1024**3) + self.assertEqual(parse_quantity("2Ti"), 2 * 1024**4) + self.assertEqual(parse_quantity("2Pi"), 2 * 1024**5) + self.assertEqual(parse_quantity("2Ei"), 2 * 1024**6) + + def test_parse_invalid(self): + self.assertRaises(ValueError, parse_quantity, "bla") + self.assertRaises(ValueError, parse_quantity, "Ki") + self.assertRaises(ValueError, parse_quantity, "M") + self.assertRaises(ValueError, parse_quantity, "2ki") + self.assertRaises(ValueError, parse_quantity, "2Ki ") + self.assertRaises(ValueError, parse_quantity, "20Ki ") + self.assertRaises(ValueError, parse_quantity, "2MiKi") + self.assertRaises(ValueError, parse_quantity, "2MK") + self.assertRaises(ValueError, parse_quantity, "2MKi") + self.assertRaises(ValueError, parse_quantity, "234df") + self.assertRaises(ValueError, parse_quantity, "df234") + self.assertRaises(ValueError, parse_quantity, tuple()) + # canonical format has no fractional digits + self.assertRaises(ValueError, parse_quantity, "2.34Ki ") + self.assertRaises(ValueError, parse_quantity, "2.34B") + self.assertRaises(ValueError, parse_quantity, "2.34Bi") + self.assertRaises(ValueError, parse_quantity, "2.34") + self.assertRaises(ValueError, parse_quantity, ".34") + self.assertRaises(ValueError, parse_quantity, "34.") + self.assertRaises(ValueError, parse_quantity, ".34M") + + +if __name__ == '__main__': + unittest.main() diff --git a/kubernetes/utils/__init__.py b/kubernetes/utils/__init__.py index 2b8597c2fb..e803697ae1 100644 --- a/kubernetes/utils/__init__.py +++ b/kubernetes/utils/__init__.py @@ -15,3 +15,4 @@ from __future__ import absolute_import from .create_from_yaml import FailToCreateError, create_from_yaml +from .quantity import parse_quantity diff --git a/kubernetes/utils/quantity.py b/kubernetes/utils/quantity.py new file mode 100644 index 0000000000..aa6a7da39e --- /dev/null +++ b/kubernetes/utils/quantity.py @@ -0,0 +1,70 @@ +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + + +def parse_quantity(quantity): + """ + Parse kubernetes canonical form quantity like 200Mi to an float number. + Supported SI suffixes: + base1024: Ki | Mi | Gi | Ti | Pi | Ei + base1000: m | "" | k | M | G | T | P | E + + Input: + quanity: string. kubernetes canonical form quantity + + Returns: + float + + Raises: + ValueError on invalid or unknown input + """ + exponents = {"m": -1, "K": 1, "k" : 1, "M": 2, + "G": 3, "T": 4, "P": 5, "E": 6} + pattern = "^(\d+)([^\d]{1,2})?$" + + if isinstance(quantity, (int, float)): + return float(quantity) + + quantity = str(quantity) + + res = re.match(pattern, quantity) + if not res: + raise ValueError( + "{} did not match pattern {}".format(quantity, pattern) + ) + number, suffix = res.groups() + number = float(number) + + if suffix is None: + return number + + suffix = res.groups()[1] + + if suffix.endswith("i"): + base = 1024 + elif len(suffix) == 1: + base = 1000 + else: + raise ValueError("{} has unknown suffix".format(quantity)) + + # handly SI inconsistency + if suffix == "ki": + raise ValueError("{} has unknown suffix".format(quantity)) + + if suffix[0] not in exponents: + raise ValueError("{} has unknown suffix".format(quantity)) + + exponent = exponents[suffix[0]] + return number * (base ** exponent)