Add utility functions to parse and format GEP-2257 Duration strings for Gateway API
Signed-off-by: Flynn <emissary@flynn.kodachi.com>
This commit is contained in:
parent
7a278c794a
commit
9d620ad4cb
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Kubernetes API Version: v1.30.1
|
Kubernetes API Version: v1.30.1
|
||||||
|
|
||||||
|
**New Feature:**
|
||||||
|
- Add utility functions to parse and format [GEP-2257] Duration strings for Gateway API
|
||||||
|
|
||||||
|
[GEP-2257]: https://gateway-api.sigs.k8s.io/geps/gep-2257/
|
||||||
|
|
||||||
# v30.1.0b1
|
# v30.1.0b1
|
||||||
|
|
||||||
|
|||||||
70
kubernetes/test/test_duration.py
Normal file
70
kubernetes/test/test_duration.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import unittest
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import kubernetes
|
||||||
|
from kubernetes.utils.duration import parse_duration, format_duration
|
||||||
|
|
||||||
|
class TestDuration(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_parse_duration(self):
|
||||||
|
# Valid durations
|
||||||
|
self.assertEqual(parse_duration("0h"), datetime.timedelta(hours=0))
|
||||||
|
self.assertEqual(parse_duration("0s"), datetime.timedelta(hours=0))
|
||||||
|
self.assertEqual(parse_duration("0h0m0s"), datetime.timedelta(hours=0))
|
||||||
|
self.assertEqual(parse_duration("1h"), datetime.timedelta(hours=1))
|
||||||
|
self.assertEqual(parse_duration("30m"), datetime.timedelta(minutes=30))
|
||||||
|
self.assertEqual(parse_duration("10s"), datetime.timedelta(seconds=10))
|
||||||
|
self.assertEqual(parse_duration("500ms"), datetime.timedelta(milliseconds=500))
|
||||||
|
self.assertEqual(parse_duration("2h30m"), datetime.timedelta(hours=2, minutes=30))
|
||||||
|
self.assertEqual(parse_duration("150m"), datetime.timedelta(hours=2, minutes=30))
|
||||||
|
self.assertEqual(parse_duration("7230s"), datetime.timedelta(hours=2, seconds=30))
|
||||||
|
self.assertEqual(parse_duration("1h30m10s"), datetime.timedelta(hours=1, minutes=30, seconds=10))
|
||||||
|
self.assertEqual(parse_duration("10s30m1h"), datetime.timedelta(hours=1, minutes=30, seconds=10))
|
||||||
|
self.assertEqual(parse_duration("100ms200ms300ms"), datetime.timedelta(milliseconds=600))
|
||||||
|
self.assertEqual(parse_duration("100ms200ms300ms"), datetime.timedelta(milliseconds=600))
|
||||||
|
|
||||||
|
# Invalid durations
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_duration("1d") # Invalid unit 'd'
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_duration("1") # Missing unit
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_duration("1m1") # Missing unit
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_duration("1h30m10s20ms50h") # Too many units
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_duration("999999h") # Too many digits
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_duration("1.5h") # Floating point is not supported
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_duration("-15m") # Negative durations are not supported
|
||||||
|
|
||||||
|
def test_format_duration(self):
|
||||||
|
# Valid durations
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(0)), "0s")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(hours=1)), "1h")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(minutes=30)), "30m")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(seconds=10)), "10s")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(milliseconds=500)), "500ms")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(hours=2, minutes=30)), "2h30m")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(hours=1, minutes=30, seconds=10)), "1h30m10s")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(milliseconds=600)), "600ms")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(hours=2, milliseconds=600)), "2h600ms")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(hours=2, minutes=30, milliseconds=600)), "2h30m600ms")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(hours=2, minutes=30, seconds=10, milliseconds=600)), "2h30m10s600ms")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(minutes=0.5)), "30s")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(seconds=0.5)), "500ms")
|
||||||
|
self.assertEqual(format_duration(datetime.timedelta(days=10)), "240h") # 10 days = 240 hours
|
||||||
|
|
||||||
|
# Invalid durations
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
format_duration(datetime.timedelta(microseconds=100)) # Sub-millisecond precision
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
format_duration(datetime.timedelta(milliseconds=0.5)) # Sub-millisecond precision
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
format_duration(datetime.timedelta(days=10000)) # Out of range (more than 99999 hours)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
format_duration(datetime.timedelta(minutes=-15)) # Negative durations are not supported
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@ -17,3 +17,4 @@ from __future__ import absolute_import
|
|||||||
from .create_from_yaml import (FailToCreateError, create_from_dict,
|
from .create_from_yaml import (FailToCreateError, create_from_dict,
|
||||||
create_from_yaml, create_from_directory)
|
create_from_yaml, create_from_directory)
|
||||||
from .quantity import parse_quantity
|
from .quantity import parse_quantity
|
||||||
|
from. duration import parse_duration
|
||||||
|
|||||||
97
kubernetes/utils/duration.py
Normal file
97
kubernetes/utils/duration.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# Copyright 2024 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 datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
import durationpy
|
||||||
|
|
||||||
|
# Initialize our RE statically, rather than compiling for every call. This has
|
||||||
|
# the downside that it'll get compiled at import time but that shouldn't
|
||||||
|
# really be a big deal.
|
||||||
|
reDuration = re.compile(r'^([0-9]{1,5}(h|m|s|ms)){1,4}$')
|
||||||
|
|
||||||
|
def parse_duration(duration) -> datetime.timedelta:
|
||||||
|
"""
|
||||||
|
Parse GEP-2257 Duration format to a datetime.timedelta object.
|
||||||
|
|
||||||
|
The GEP-2257 Duration format is a restricted form of the input to the Go
|
||||||
|
time.ParseDuration function; specifically, it must match the regex
|
||||||
|
"^([0-9]{1,5}(h|m|s|ms)){1,4}$".
|
||||||
|
|
||||||
|
See https://gateway-api.sigs.k8s.io/geps/gep-2257/ for more details.
|
||||||
|
|
||||||
|
Input: duration: string
|
||||||
|
|
||||||
|
Returns: datetime.timedelta
|
||||||
|
|
||||||
|
Raises: ValueError on invalid or unknown input
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not reDuration.match(duration):
|
||||||
|
raise ValueError("Invalid duration format: {}".format(duration))
|
||||||
|
|
||||||
|
return durationpy.from_str(duration)
|
||||||
|
|
||||||
|
def format_duration(delta: datetime.timedelta) -> str:
|
||||||
|
"""
|
||||||
|
Format a datetime.timedelta object to GEP-2257 Duration format.
|
||||||
|
|
||||||
|
The GEP-2257 Duration format is a restricted form of the input to the Go
|
||||||
|
time.ParseDuration function; specifically, it must match the regex
|
||||||
|
"^([0-9]{1,5}(h|m|s|ms)){1,4}$".
|
||||||
|
|
||||||
|
See https://gateway-api.sigs.k8s.io/geps/gep-2257/ for more details.
|
||||||
|
|
||||||
|
Input: duration: datetime.timedelta
|
||||||
|
|
||||||
|
Returns: string
|
||||||
|
|
||||||
|
Raises: ValueError if the timedelta given cannot be expressed as a
|
||||||
|
GEP-2257 Duration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Short-circuit if we have a zero delta.
|
||||||
|
if delta == datetime.timedelta(0):
|
||||||
|
return "0s"
|
||||||
|
|
||||||
|
# durationpy.to_str() is happy to use floating-point seconds, which
|
||||||
|
# GEP-2257 is _not_ happy with. So start by peeling off any microseconds
|
||||||
|
# from our delta.
|
||||||
|
delta_us = delta.microseconds
|
||||||
|
|
||||||
|
if (delta_us % 1000) != 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot express sub-millisecond precision in GEP-2257: {}"
|
||||||
|
.format(delta)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Second short-circuit.
|
||||||
|
|
||||||
|
delta -= datetime.timedelta(microseconds=delta_us)
|
||||||
|
delta_ms = delta_us // 1000
|
||||||
|
delta_str = durationpy.to_str(delta)
|
||||||
|
|
||||||
|
if delta_ms > 0:
|
||||||
|
# We have milliseconds to add back in. Make sure to not have a leading
|
||||||
|
# "0" if we have no other duration components.
|
||||||
|
if delta == datetime.timedelta(0):
|
||||||
|
delta_str = ""
|
||||||
|
|
||||||
|
delta_str += f"{delta_ms}ms"
|
||||||
|
|
||||||
|
if not reDuration.match(delta_str):
|
||||||
|
raise ValueError("Invalid duration format: {}".format(durationpy.to_str(delta)))
|
||||||
|
|
||||||
|
return delta_str
|
||||||
|
|
||||||
@ -9,3 +9,4 @@ requests # Apache-2.0
|
|||||||
requests-oauthlib # ISC
|
requests-oauthlib # ISC
|
||||||
oauthlib>=3.2.2 # BSD
|
oauthlib>=3.2.2 # BSD
|
||||||
urllib3>=1.24.2 # MIT
|
urllib3>=1.24.2 # MIT
|
||||||
|
durationpy>=0.7 # MIT
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user