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
|
||||
|
||||
**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
|
||||
|
||||
|
||||
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,
|
||||
create_from_yaml, create_from_directory)
|
||||
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
|
||||
oauthlib>=3.2.2 # BSD
|
||||
urllib3>=1.24.2 # MIT
|
||||
durationpy>=0.7 # MIT
|
||||
|
||||
Loading…
Reference in New Issue
Block a user