os-normalizer 0.4.3__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of os-normalizer might be problematic. Click here for more details.

Files changed (36) hide show
  1. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/CHANGELOG.md +18 -0
  2. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/PKG-INFO +2 -1
  3. os_normalizer-0.5.0/RELEASING.md +20 -0
  4. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/constants.py +139 -2
  5. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/cpe.py +57 -12
  6. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/helpers.py +26 -15
  7. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/models.py +19 -9
  8. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/os_normalizer.py +76 -49
  9. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/__init__.py +7 -3
  10. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/bsd.py +2 -1
  11. os_normalizer-0.5.0/os_normalizer/parsers/esxi.py +83 -0
  12. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/linux.py +6 -5
  13. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/macos.py +12 -14
  14. os_normalizer-0.5.0/os_normalizer/parsers/mobile.py +55 -0
  15. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/network/__init__.py +5 -1
  16. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/network/cisco.py +13 -7
  17. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/network/fortinet.py +15 -5
  18. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/network/huawei.py +9 -3
  19. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/network/juniper.py +10 -4
  20. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/network/netgear.py +10 -3
  21. os_normalizer-0.5.0/os_normalizer/parsers/solaris.py +100 -0
  22. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/parsers/windows.py +7 -6
  23. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/pyproject.toml +11 -1
  24. os_normalizer-0.4.3/RELEASING.md +0 -71
  25. os_normalizer-0.4.3/os_normalizer/parsers/mobile.py +0 -37
  26. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/.gitignore +0 -0
  27. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/.python-version +0 -0
  28. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/.uv-cache/.gitignore +0 -0
  29. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/.uv-cache/.lock +0 -0
  30. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/.uv-cache/CACHEDIR.TAG +0 -0
  31. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/.uv-cache/interpreter-v4/7e11d242fb84b9e8/939db8dea853eb17.msgpack +0 -0
  32. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/.uv-cache/sdists-v9/.git +0 -0
  33. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/.uv-cache/sdists-v9/.gitignore +0 -0
  34. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/LICENSE +0 -0
  35. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/README.md +0 -0
  36. {os_normalizer-0.4.3 → os_normalizer-0.5.0}/os_normalizer/__init__.py +0 -0
@@ -3,6 +3,24 @@
3
3
  All notable changes to this project are documented here.
4
4
  This file adheres to Keep a Changelog and Semantic Versioning.
5
5
 
6
+ ## `v0.5.0` — [2025-10-30]
7
+
8
+ - Update release docs, add 3.14 classifier, sort imports [b7e07de]
9
+ - Add a ton more architectures and extract into constants [bd481f8]
10
+ - Add esxi and solaris support [8c14505]
11
+ - Set the unreleased tag for uv-ship [791361c]
12
+
13
+ ## `v0.4.4` — [2025-10-25]
14
+
15
+ - Add uv-ship config [afd3bed]
16
+ - Merge pull request #2 from johnscillieri/codex/refactor-family-strings-into-constants [cf84c0f]
17
+ - Polish enum usage and update changelog [c3fb7f4]
18
+ - Merge pull request #1 from johnscillieri/codex/add-support-for-huawei-harmonyos [45fe0f9]
19
+ - Document HarmonyOS support addition [584ed2f]
20
+ - Added: HarmonyOS detection for Huawei devices, including dedicated parsing and metadata.
21
+ - Added: HarmonyOS normalization captures build identifiers (e.g., `5.0.0.107`) and propagates them into generated CPE data.
22
+ - Changed: Replaced string literals for OS families and precision tiers with shared enums and ordering constants across the normalization pipeline.
23
+
6
24
  ## [0.4.3] - 2025-10-20
7
25
 
8
26
  - Fixed: Windows build parsing and inconsistent strings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: os-normalizer
3
- Version: 0.4.3
3
+ Version: 0.5.0
4
4
  Summary: Normalize raw OS strings/metadata into structured data (family, product, version, arch).
5
5
  Project-URL: Homepage, https://github.com/johnscillieri/os-normalizer
6
6
  Project-URL: Repository, https://github.com/johnscillieri/os-normalizer
@@ -38,6 +38,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
38
38
  Classifier: Programming Language :: Python :: 3.11
39
39
  Classifier: Programming Language :: Python :: 3.12
40
40
  Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Programming Language :: Python :: 3.14
41
42
  Classifier: Topic :: Software Development :: Libraries
42
43
  Classifier: Topic :: System :: Operating System
43
44
  Requires-Python: >=3.11
@@ -0,0 +1,20 @@
1
+ # Releasing to PyPI
2
+
3
+ This project uses a modern PEP 621 `pyproject.toml` with the Hatchling build backend. Below are the steps to build and publish to PyPI using uv-ship.
4
+
5
+ ## Prereqs
6
+
7
+ - Python 3.11+
8
+ - `uv` installed (https://github.com/astral-sh/uv)
9
+ - `uv-ship` installed (via `uv tool install uv-ship`)
10
+ - PyPI accounts and API tokens for TestPyPI and/or PyPI
11
+
12
+ ## Run tests and lint
13
+
14
+ - `uv run pytest`
15
+ - Optionally: `uv run nox`
16
+
17
+ ## Releasing
18
+
19
+ - `uv-ship next <type>` where type is major|minor|patch
20
+ - Follow the prompts and release the new version.
@@ -1,19 +1,156 @@
1
1
  """Constants and static lookup tables for the OS fingerprinting package."""
2
2
 
3
+ from enum import StrEnum
4
+
5
+
6
+ class OSFamily(StrEnum):
7
+ """Canonical OS family identifiers used throughout the project."""
8
+
9
+ ANDROID = "android"
10
+ BSD = "bsd"
11
+ ESXI = "esxi"
12
+ HARMONYOS = "harmonyos"
13
+ IOS = "ios"
14
+ LINUX = "linux"
15
+ MACOS = "macos"
16
+ NETWORK = "network-os"
17
+ SOLARIS = "solaris"
18
+ WINDOWS = "windows"
19
+
20
+
21
+ class PrecisionLevel(StrEnum):
22
+ """Granularity levels for parsed OS version information."""
23
+
24
+ UNKNOWN = "unknown"
25
+ FAMILY = "family"
26
+ PRODUCT = "product"
27
+ MAJOR = "major"
28
+ MINOR = "minor"
29
+ PATCH = "patch"
30
+ BUILD = "build"
31
+
32
+
33
+ PRECISION_ORDER = {
34
+ PrecisionLevel.BUILD: 6,
35
+ PrecisionLevel.PATCH: 5,
36
+ PrecisionLevel.MINOR: 4,
37
+ PrecisionLevel.MAJOR: 3,
38
+ PrecisionLevel.PRODUCT: 2,
39
+ PrecisionLevel.FAMILY: 1,
40
+ PrecisionLevel.UNKNOWN: 0,
41
+ }
42
+
43
+
44
+ # Architecture tokens recognised across parsers.
45
+ ARCHITECTURE_TOKENS = (
46
+ "x86_64",
47
+ "amd64",
48
+ "x64",
49
+ "x86",
50
+ "ia32",
51
+ "i386",
52
+ "i486",
53
+ "i586",
54
+ "i686",
55
+ "arm64",
56
+ "aarch64",
57
+ "arm64e",
58
+ "armv8",
59
+ "armv8l",
60
+ "arm",
61
+ "armv7",
62
+ "armv7l",
63
+ "armv7hf",
64
+ "armv6",
65
+ "armhf",
66
+ "armel",
67
+ "ppc64le",
68
+ "powerpc64le",
69
+ "ppc64",
70
+ "powerpc64",
71
+ "ppc",
72
+ "ppc32",
73
+ "ppcle",
74
+ "powerpc",
75
+ "sparc",
76
+ "sparc64",
77
+ "sparcv9",
78
+ "sun4u",
79
+ "sun4v",
80
+ "mips",
81
+ "mips32",
82
+ "mipsel",
83
+ "mips64",
84
+ "mips64el",
85
+ "mips64le",
86
+ "s390x",
87
+ "s390",
88
+ "riscv64",
89
+ "riscv",
90
+ "loongarch64",
91
+ "tilegx",
92
+ "alpha",
93
+ "ia64",
94
+ "itanium",
95
+ "hppa",
96
+ "parisc",
97
+ "m68k",
98
+ )
99
+
3
100
  # Architecture synonyms
4
101
  ARCH_SYNONYMS = {
5
- "x64": "x86_64",
6
102
  "x86_64": "x86_64",
7
103
  "amd64": "x86_64",
104
+ "x64": "x86_64",
8
105
  "x86": "x86",
106
+ "ia32": "x86",
9
107
  "i386": "x86",
108
+ "i486": "x86",
109
+ "i586": "x86",
10
110
  "i686": "x86",
11
- "aarch64": "arm64",
12
111
  "arm64": "arm64",
112
+ "aarch64": "arm64",
113
+ "arm64e": "arm64",
13
114
  "armv8": "arm64",
115
+ "armv8l": "arm64",
116
+ "arm": "arm",
14
117
  "armv7": "arm",
15
118
  "armv7l": "arm",
119
+ "armv7hf": "arm",
120
+ "armv6": "arm",
121
+ "armhf": "arm",
122
+ "armel": "arm",
16
123
  "ppc64le": "ppc64le",
124
+ "powerpc64le": "ppc64le",
125
+ "ppc64": "ppc64",
126
+ "powerpc64": "ppc64",
127
+ "ppc": "ppc",
128
+ "ppc32": "ppc",
129
+ "ppcle": "ppc",
130
+ "powerpc": "ppc",
131
+ "sparc": "sparc",
132
+ "sparc64": "sparc",
133
+ "sparcv9": "sparc",
134
+ "sun4u": "sparc",
135
+ "sun4v": "sparc",
136
+ "mips": "mips",
137
+ "mips32": "mips",
138
+ "mipsel": "mips",
139
+ "mips64": "mips64",
140
+ "mips64el": "mips64",
141
+ "mips64le": "mips64",
142
+ "s390x": "s390x",
143
+ "s390": "s390x",
144
+ "riscv64": "riscv64",
145
+ "riscv": "riscv64",
146
+ "loongarch64": "loongarch64",
147
+ "tilegx": "tilegx",
148
+ "alpha": "alpha",
149
+ "ia64": "ia64",
150
+ "itanium": "ia64",
151
+ "hppa": "parisc",
152
+ "parisc": "parisc",
153
+ "m68k": "m68k",
17
154
  }
18
155
 
19
156
  # Windows build map (build number range -> product name, marketing channel)
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  from typing import TYPE_CHECKING
11
11
 
12
- from os_normalizer.constants import WINDOWS_BUILD_MAP
12
+ from os_normalizer.constants import OSFamily, WINDOWS_BUILD_MAP
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from .models import OSData
@@ -39,12 +39,19 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
39
39
 
40
40
  strategy controls version/update selection rules.
41
41
  """
42
- fam = (p.family or "").lower()
42
+ raw_family = p.family
43
+ family = (
44
+ raw_family
45
+ if isinstance(raw_family, OSFamily)
46
+ else OSFamily._value2member_map_.get(str(raw_family).lower())
47
+ if raw_family
48
+ else None
49
+ )
43
50
  vendor = (p.vendor or "").lower() if p.vendor else None
44
51
  product = (p.product or "").lower() if p.product else ""
45
52
 
46
53
  # Windows
47
- if fam == "windows":
54
+ if family == OSFamily.WINDOWS:
48
55
  vtok = "microsoft"
49
56
  prod_map = {
50
57
  "windows 7": "windows_7",
@@ -68,11 +75,11 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
68
75
  return vtok, base, "windows"
69
76
 
70
77
  # macOS
71
- if fam == "macos":
78
+ if family == OSFamily.MACOS:
72
79
  return "apple", "macos", "macos"
73
80
 
74
81
  # Linux distros (use distro when present)
75
- if fam == "linux":
82
+ if family == OSFamily.LINUX:
76
83
  d = (p.distro or "").lower()
77
84
  if d == "ubuntu":
78
85
  return "canonical", "ubuntu_linux", "ubuntu"
@@ -93,7 +100,7 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
93
100
  return vendor or "linux", product or "linux", "linux"
94
101
 
95
102
  # BSDs
96
- if fam == "bsd":
103
+ if family == OSFamily.BSD:
97
104
  if product and "freebsd" in product:
98
105
  return "freebsd", "freebsd", "freebsd"
99
106
  if product and "openbsd" in product:
@@ -102,8 +109,14 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
102
109
  return "netbsd", "netbsd", "netbsd"
103
110
  return vendor or "bsd", product or "bsd", "bsd"
104
111
 
112
+ if family == OSFamily.SOLARIS:
113
+ return "oracle", "solaris", "solaris"
114
+
115
+ if family == OSFamily.ESXI:
116
+ return "vmware", "esxi", "esxi"
117
+
105
118
  # Network OS
106
- if fam == "network-os":
119
+ if family == OSFamily.NETWORK:
107
120
  if vendor == "cisco":
108
121
  if product and ("ios xe" in product or "ios-xe" in product):
109
122
  return "cisco", "ios_xe", "ios_xe"
@@ -120,13 +133,20 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
120
133
  return vendor or "network", (product or "firmware").replace(" ", "_"), "firmware"
121
134
 
122
135
  # Mobile
123
- if fam == "android":
124
- return "google", "android", "android"
125
- if fam == "ios":
126
- return "apple", "iphone_os", "ios"
136
+ if family == OSFamily.ANDROID:
137
+ return "google", "android", OSFamily.ANDROID.value
138
+ if family == OSFamily.IOS:
139
+ return "apple", "iphone_os", OSFamily.IOS.value
140
+ if family == OSFamily.HARMONYOS:
141
+ return "huawei", "harmonyos", OSFamily.HARMONYOS.value
127
142
 
128
143
  # Fallback
129
- return (vendor or fam or "unknown"), (product or (fam or "unknown")).replace(" ", "_"), fam or "unknown"
144
+ family_value = family.value if family else (str(raw_family).lower() if raw_family else "unknown")
145
+ return (
146
+ vendor or family_value or "unknown",
147
+ (product or family_value).replace(" ", "_"),
148
+ family_value,
149
+ )
130
150
 
131
151
 
132
152
  def _fmt_version(p: OSData, strategy: str) -> tuple[str, str, str]:
@@ -192,6 +212,18 @@ def _fmt_version(p: OSData, strategy: str) -> tuple[str, str, str]:
192
212
  ver = "*"
193
213
  return ver, "*", (edition or "*")
194
214
 
215
+ if strategy in ("solaris", "esxi"):
216
+ if maj is not None:
217
+ ver = str(maj)
218
+ if minr is not None:
219
+ ver = f"{ver}.{minr}"
220
+ if pat is not None:
221
+ ver = f"{ver}.{pat}"
222
+ else:
223
+ ver = "*"
224
+ update = build or "*"
225
+ return ver, update, "*"
226
+
195
227
  if strategy == "fortios":
196
228
  if maj is not None and minr is not None and pat is not None:
197
229
  ver = f"{maj}.{minr}.{pat}"
@@ -216,6 +248,19 @@ def _fmt_version(p: OSData, strategy: str) -> tuple[str, str, str]:
216
248
  ver = "*"
217
249
  return ver, "*", "*"
218
250
 
251
+ if strategy == "harmonyos":
252
+ if maj is not None:
253
+ if minr is not None and pat is not None:
254
+ ver = f"{maj}.{minr}.{pat}"
255
+ elif minr is not None:
256
+ ver = f"{maj}.{minr}"
257
+ else:
258
+ ver = f"{maj}"
259
+ else:
260
+ ver = "*"
261
+ update = build or "*"
262
+ return ver, update, "*"
263
+
219
264
  # Generic fallback
220
265
  if build:
221
266
  ver = build
@@ -3,7 +3,7 @@
3
3
  import re
4
4
  from typing import Any
5
5
 
6
- from .constants import ARCH_SYNONYMS
6
+ from .constants import ARCH_SYNONYMS, ARCHITECTURE_TOKENS, PrecisionLevel
7
7
  from .models import OSData
8
8
 
9
9
 
@@ -34,17 +34,17 @@ def precision_from_parts(
34
34
  minor: int | None,
35
35
  patch: int | None,
36
36
  build: str | None,
37
- ) -> str:
37
+ ) -> PrecisionLevel:
38
38
  """Derive a precision label from version components."""
39
39
  if build:
40
- return "build"
40
+ return PrecisionLevel.BUILD
41
41
  if patch is not None:
42
- return "patch"
42
+ return PrecisionLevel.PATCH
43
43
  if minor is not None:
44
- return "minor"
44
+ return PrecisionLevel.MINOR
45
45
  if major is not None:
46
- return "major"
47
- return "product"
46
+ return PrecisionLevel.MAJOR
47
+ return PrecisionLevel.PRODUCT
48
48
 
49
49
 
50
50
  def canonical_key(p: OSData) -> str:
@@ -61,7 +61,10 @@ def canonical_key(p: OSData) -> str:
61
61
 
62
62
 
63
63
  # Regex for extracting an architecture token from free-form text
64
- ARCH_TEXT_RE = re.compile(r"\b(x86_64|amd64|x64|x86|i386|i686|arm64|aarch64|armv8|armv7l?|ppc64le)\b", re.IGNORECASE)
64
+ _ARCH_PATTERN = "|".join(
65
+ sorted((re.escape(token) for token in ARCHITECTURE_TOKENS), key=len, reverse=True)
66
+ )
67
+ ARCH_TEXT_RE = re.compile(rf"\b({_ARCH_PATTERN})\b", re.IGNORECASE)
65
68
 
66
69
 
67
70
  def extract_arch_from_text(text: str) -> str | None:
@@ -92,16 +95,24 @@ def parse_os_release(blob_text: str) -> dict[str, Any]:
92
95
  return out
93
96
 
94
97
 
95
- def update_confidence(p: OSData, precision: str) -> None:
98
+ def update_confidence(p: OSData, precision: PrecisionLevel | str) -> None:
96
99
  """Boost confidence based on the determined precision level.
97
100
 
98
101
  The mapping mirrors the original ad-hoc values used throughout the monolithic file.
99
102
  """
103
+ if isinstance(precision, PrecisionLevel):
104
+ level = precision
105
+ else:
106
+ try:
107
+ level = PrecisionLevel(str(precision))
108
+ except ValueError:
109
+ level = PrecisionLevel.UNKNOWN
110
+
100
111
  boost_map = {
101
- "build": 0.85,
102
- "patch": 0.80,
103
- "minor": 0.75,
104
- "major": 0.70,
105
- "product": 0.60,
112
+ PrecisionLevel.BUILD: 0.85,
113
+ PrecisionLevel.PATCH: 0.80,
114
+ PrecisionLevel.MINOR: 0.75,
115
+ PrecisionLevel.MAJOR: 0.70,
116
+ PrecisionLevel.PRODUCT: 0.60,
106
117
  }
107
- p.confidence = max(p.confidence, boost_map.get(precision, 0.5))
118
+ p.confidence = max(p.confidence, boost_map.get(level, 0.5))
@@ -2,16 +2,20 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from dataclasses import dataclass, field, fields as dataclass_fields
5
+ from dataclasses import dataclass, field
6
+ from dataclasses import fields as dataclass_fields
7
+ from enum import Enum
6
8
  from typing import Any
7
9
 
10
+ from .constants import OSFamily, PrecisionLevel
11
+
8
12
 
9
13
  @dataclass
10
14
  class OSData:
11
15
  """Structured representation of a parsed operating system."""
12
16
 
13
17
  # Core identity
14
- family: str | None = None # windows, linux, macos, ios, android, bsd, network-os
18
+ family: OSFamily | None = None # windows, linux, macos, ios, android, bsd, solaris, esxi, network-os
15
19
  vendor: str | None = None # Microsoft, Apple, Canonical, Cisco, Juniper, Fortinet, Huawei, Netgear…
16
20
  product: str | None = None # Windows 11, Ubuntu, macOS, IOS XE, Junos, FortiOS, VRP, Firmware
17
21
  edition: str | None = None # Pro/Enterprise/LTSC; universalk9/ipbase; etc.
@@ -37,7 +41,7 @@ class OSData:
37
41
  build_id: str | None = None
38
42
 
39
43
  # Meta information
40
- precision: str = "unknown" # family|product|major|minor|patch|build
44
+ precision: PrecisionLevel = PrecisionLevel.UNKNOWN # family|product|major|minor|patch|build
41
45
  confidence: float = 0.0
42
46
  evidence: dict[str, Any] = field(default_factory=dict)
43
47
 
@@ -47,7 +51,7 @@ class OSData:
47
51
  def __str__(self) -> str: # pragma: no cover - formatting helper
48
52
  parts: list[str] = []
49
53
 
50
- if self.family == "windows":
54
+ if self.family == OSFamily.WINDOWS:
51
55
  return _format_windows(self)
52
56
 
53
57
  # Prefer vendor + product; fallback to pretty_name; then family
@@ -57,7 +61,10 @@ class OSData:
57
61
  elif self.pretty_name:
58
62
  parts.append(self.pretty_name)
59
63
  else:
60
- parts.append(self.family or "Unknown OS")
64
+ if isinstance(self.family, OSFamily):
65
+ parts.append(self.family.value)
66
+ else:
67
+ parts.append(self.family or "Unknown OS")
61
68
 
62
69
  # Version string (major[.minor[.patch]]) and optional build
63
70
  ver_chunks: list[str] = []
@@ -100,8 +107,9 @@ class OSData:
100
107
  parts.append(f"[build: {self.build_id}]")
101
108
 
102
109
  # Precision/confidence summary
103
- if self.precision and self.precision != "unknown":
104
- parts.append(f"{{{self.precision}:{self.confidence:.2f}}}")
110
+ if self.precision and self.precision != PrecisionLevel.UNKNOWN:
111
+ label = self.precision.value if isinstance(self.precision, PrecisionLevel) else str(self.precision)
112
+ parts.append(f"{{{label}:{self.confidence:.2f}}}")
105
113
  elif self.confidence:
106
114
  parts.append(f"{{{self.confidence:.2f}}}")
107
115
 
@@ -129,6 +137,8 @@ class OSData:
129
137
  sval = none_str
130
138
  elif name == "confidence" and isinstance(val, (int, float)):
131
139
  sval = f"{float(val):.2f}"
140
+ elif isinstance(val, Enum):
141
+ sval = val.value
132
142
  elif isinstance(val, list):
133
143
  sval = ", ".join(str(x) for x in val)
134
144
  elif isinstance(val, dict):
@@ -159,7 +169,7 @@ def _format_windows(p: OSData) -> str:
159
169
 
160
170
  if __name__ == "__main__":
161
171
  x = OSData(
162
- family="linux",
172
+ family=OSFamily.LINUX,
163
173
  vendor="Fedora Project",
164
174
  product="Fedora Linux",
165
175
  version_major=33,
@@ -168,7 +178,7 @@ if __name__ == "__main__":
168
178
  distro="fedora",
169
179
  like_distros=[],
170
180
  pretty_name="Fedora Linux",
171
- precision="major",
181
+ precision=PrecisionLevel.MAJOR,
172
182
  confidence=0.7,
173
183
  evidence={"hit": "linux"},
174
184
  )