|
16 | 16 | import asyncio |
17 | 17 | import fnmatch |
18 | 18 | import inspect |
19 | | -import logging |
20 | 19 | import re |
21 | 20 | import threading |
22 | | -import time |
23 | 21 | import unicodedata |
24 | 22 | import uuid |
| 23 | +from datetime import datetime, timezone |
25 | 24 | from platform import machine, processor, system |
26 | 25 | from types import MappingProxyType |
27 | 26 | from typing import Any, Callable, Generic, Iterable, Optional, Sized, TypeVar, Union |
|
34 | 33 | except ImportError: |
35 | 34 | import json # type: ignore |
36 | 35 |
|
37 | | -logger: logging.Logger = logging.getLogger(__name__) |
| 36 | +ISO_MICRO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f%z" |
38 | 37 | _T = TypeVar("_T") |
39 | 38 | ATTRIBUTE_LENGTH_LIMIT: int = 128 |
40 | 39 | ATTRIBUTE_NUMBER_LIMIT: int = 256 |
@@ -277,9 +276,12 @@ def verify_value_length(attributes: list[dict]) -> Optional[list[dict]]: |
277 | 276 | return result |
278 | 277 |
|
279 | 278 |
|
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)) |
283 | 285 |
|
284 | 286 |
|
285 | 287 | def uri_join(*uri_parts: str) -> str: |
@@ -586,3 +588,102 @@ def clean_binary_characters(text: str) -> str: |
586 | 588 | if not text: |
587 | 589 | return "" |
588 | 590 | 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 |
0 commit comments