Skip to content

Commit d3220be

Browse files
committed
Add auxiliary code for microseconds
1 parent 5ed2e67 commit d3220be

3 files changed

Lines changed: 143 additions & 6 deletions

File tree

reportportal_client/helpers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
calculate_file_part_size,
1919
calculate_json_part_size,
2020
caseless_equal,
21+
compare_semantic_versions,
2122
dict_to_payload,
2223
gen_attributes,
2324
generate_uuid,
@@ -56,6 +57,7 @@
5657
"calculate_file_part_size",
5758
"calculate_json_part_size",
5859
"caseless_equal",
60+
"compare_semantic_versions",
5961
"dict_to_payload",
6062
"gen_attributes",
6163
"generate_uuid",

reportportal_client/helpers/common_helpers.py

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616
import asyncio
1717
import fnmatch
1818
import inspect
19-
import logging
2019
import re
2120
import threading
22-
import time
2321
import unicodedata
2422
import uuid
23+
from datetime import datetime, timezone
2524
from platform import machine, processor, system
2625
from types import MappingProxyType
2726
from typing import Any, Callable, Generic, Iterable, Optional, Sized, TypeVar, Union
@@ -34,7 +33,7 @@
3433
except ImportError:
3534
import json # type: ignore
3635

37-
logger: logging.Logger = logging.getLogger(__name__)
36+
ISO_MICRO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f%z"
3837
_T = TypeVar("_T")
3938
ATTRIBUTE_LENGTH_LIMIT: int = 128
4039
ATTRIBUTE_NUMBER_LIMIT: int = 256
@@ -277,9 +276,12 @@ def verify_value_length(attributes: list[dict]) -> Optional[list[dict]]:
277276
return result
278277

279278

280-
def timestamp() -> str:
281-
"""Return string representation of the current time in milliseconds."""
282-
return str(int(time.time() * 1000))
279+
def timestamp(use_microseconds=False) -> str:
280+
"""Return string representation of the current time in milli/microseconds."""
281+
now = datetime.now(tz=timezone.utc)
282+
if use_microseconds:
283+
return now.strftime(ISO_MICRO_FORMAT)
284+
return str(int(now.timestamp() * 1000))
283285

284286

285287
def uri_join(*uri_parts: str) -> str:
@@ -586,3 +588,102 @@ def clean_binary_characters(text: str) -> str:
586588
if not text:
587589
return ""
588590
return text.translate(CLEANUP_TABLE)
591+
592+
593+
def compare_semantic_versions(compared: str, basic: str) -> int:
594+
"""Compare semantic versions using SemVer precedence rules."""
595+
compared_norm = _normalize_version(compared)
596+
basic_norm = _normalize_version(basic)
597+
598+
compared_base, compared_pre_release = _split_base_and_pre_release(compared_norm)
599+
basic_base, basic_pre_release = _split_base_and_pre_release(basic_norm)
600+
601+
core_comparison = _compare_core_versions(compared_base, basic_base)
602+
if core_comparison != 0:
603+
return core_comparison
604+
605+
if compared_pre_release == basic_pre_release:
606+
return 0
607+
if compared_pre_release is None:
608+
return 1
609+
if basic_pre_release is None:
610+
return -1
611+
return _compare_pre_release(compared_pre_release, basic_pre_release)
612+
613+
614+
def _normalize_version(version: str) -> str:
615+
normalized_version = version.strip()
616+
if normalized_version.startswith("v") or normalized_version.startswith("V"):
617+
normalized_version = normalized_version[1:]
618+
plus_index = normalized_version.find("+")
619+
if plus_index >= 0:
620+
normalized_version = normalized_version[:plus_index]
621+
return normalized_version
622+
623+
624+
def _split_base_and_pre_release(version: str) -> tuple[str, Optional[str]]:
625+
dash_index = version.find("-")
626+
if dash_index < 0:
627+
return version, None
628+
return version[:dash_index], version[dash_index + 1 :]
629+
630+
631+
def _compare_core_versions(core_1: str, core_2: str) -> int:
632+
parts_1 = _split_without_trailing_empty_segments(core_1, ".")
633+
parts_2 = _split_without_trailing_empty_segments(core_2, ".")
634+
for i in range(max(len(parts_1), len(parts_2))):
635+
part_1 = _parse_int_safe(parts_1[i]) if i < len(parts_1) else 0
636+
part_2 = _parse_int_safe(parts_2[i]) if i < len(parts_2) else 0
637+
if part_1 != part_2:
638+
return -1 if part_1 < part_2 else 1
639+
return 0
640+
641+
642+
def _compare_pre_release(pre_release_1: str, pre_release_2: str) -> int:
643+
tokens_1 = _split_without_trailing_empty_segments(pre_release_1, ".")
644+
tokens_2 = _split_without_trailing_empty_segments(pre_release_2, ".")
645+
for i in range(max(len(tokens_1), len(tokens_2))):
646+
token_1: Optional[str] = tokens_1[i] if i < len(tokens_1) else None
647+
token_2: Optional[str] = tokens_2[i] if i < len(tokens_2) else None
648+
if token_1 == token_2:
649+
continue
650+
if token_1 is None:
651+
return -1
652+
if token_2 is None:
653+
return 1
654+
655+
token_1_is_numeric = _is_numeric(token_1)
656+
token_2_is_numeric = _is_numeric(token_2)
657+
if token_1_is_numeric and token_2_is_numeric:
658+
number_1 = _parse_int_safe(token_1)
659+
number_2 = _parse_int_safe(token_2)
660+
if number_1 != number_2:
661+
return -1 if number_1 < number_2 else 1
662+
elif token_1_is_numeric != token_2_is_numeric:
663+
return -1 if token_1_is_numeric else 1
664+
else:
665+
if token_1 < token_2:
666+
return -1
667+
if token_1 > token_2:
668+
return 1
669+
return 0
670+
671+
672+
def _parse_int_safe(value: str) -> int:
673+
try:
674+
return int(value)
675+
except ValueError:
676+
return 0
677+
678+
679+
def _is_numeric(value: str) -> bool:
680+
if not value:
681+
return False
682+
return value.isdigit()
683+
684+
685+
def _split_without_trailing_empty_segments(value: str, separator: str) -> list[str]:
686+
parts = value.split(separator)
687+
while len(parts) > 1 and parts[-1] == "":
688+
parts.pop()
689+
return parts

tests/helpers/test_helpers.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from reportportal_client.helpers import (
2222
ATTRIBUTE_LENGTH_LIMIT,
2323
TRUNCATE_REPLACEMENT,
24+
compare_semantic_versions,
2425
gen_attributes,
2526
get_launch_sys_attrs,
2627
guess_content_type_from_bytes,
@@ -241,3 +242,36 @@ def test_to_bool_invalid_value():
241242
)
242243
def test_match_with_glob_pattern(pattern: Optional[str], line: Optional[str], expected: bool):
243244
assert match_pattern(translate_glob_to_regex(pattern), line) == expected
245+
246+
247+
@pytest.mark.parametrize(
248+
["compared", "basic", "expected"],
249+
[
250+
("5.13.2", "5.13.2", 0),
251+
("5.13.1", "5.13.2", -1),
252+
("5.13.3", "5.13.2", 1),
253+
("5.12.2", "5.13.2", -1),
254+
("5.14.2", "5.13.2", 1),
255+
("4.13.2", "5.13.2", -1),
256+
("6.13.2", "5.13.2", 1),
257+
("v5.13.2", "5.13.2", 0),
258+
("v5.13.1", "v5.13.2", -1),
259+
("5.13.3+12345", "5.13.2+54321", 1),
260+
("5.13.2", "5.13.2+54321", 0),
261+
("5.13.2-1.1", "5.13.2-1.1", 0),
262+
("5.13.2-1.2", "5.13.2-1.1", 1),
263+
("5.13.2-0.9", "5.13.2-1.1", -1),
264+
("5.13.2-1.0", "5.13.2-1.1", -1),
265+
("5.13.2-1", "5.13.2-1.1", -1),
266+
("5.13.2-1.1", "5.13.2-1", 1),
267+
("5.13.2-1.", "5.13.2-1", 0),
268+
("5.13.2-1.", "5.13.2-1.1", -1),
269+
("5.13.2-1.a", "5.13.2-1.1", 1),
270+
("5.13.2-1.1", "5.13.2-1.a", -1),
271+
("5.13.2-1.a", "5.13.2-1.a", 0),
272+
("5.13.2-1.b", "5.13.2-1.a", 1),
273+
("5.13.2-1.a", "5.13.2-1.b", -1),
274+
],
275+
)
276+
def test_compare_semver(compared: str, basic: str, expected: int):
277+
assert compare_semantic_versions(compared, basic) == expected

0 commit comments

Comments
 (0)