os-normalizer 0.4.3__py3-none-any.whl → 0.5.0__py3-none-any.whl

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.

@@ -3,6 +3,7 @@
3
3
  import re
4
4
  from typing import Any
5
5
 
6
+ from os_normalizer.constants import PrecisionLevel
6
7
  from os_normalizer.helpers import (
7
8
  parse_semver_like,
8
9
  precision_from_parts,
@@ -48,7 +49,7 @@ def parse_bsd(text: str, data: dict[str, Any], p: OSData) -> OSData:
48
49
  # Prefer variant-anchored version pattern; fall back to generic semver
49
50
  x, y, z = _extract_version(text)
50
51
  p.version_major, p.version_minor, p.version_patch = x, y, z
51
- p.precision = precision_from_parts(x, y, z, None) if x else "product"
52
+ p.precision = precision_from_parts(x, y, z, None) if x else PrecisionLevel.PRODUCT
52
53
 
53
54
  # Channel from explicit markers/suffixes
54
55
  ch = BSD_CHANNEL_RE.search(text)
@@ -0,0 +1,83 @@
1
+ """VMware ESXi specific parsing logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from os_normalizer.constants import PrecisionLevel
9
+ from os_normalizer.helpers import parse_semver_like, precision_from_parts, update_confidence
10
+ from os_normalizer.models import OSData
11
+
12
+ ESXI_PRODUCT_RE = re.compile(
13
+ r"VMware\s+ESXi\s+(\d+(?:\.\d+){1,3})(?:\s+(?:build|Build)\s*[-#]?(\d+))?",
14
+ re.IGNORECASE,
15
+ )
16
+ VMKERNEL_RE = re.compile(
17
+ r"VMkernel\s+\S+\s+(\d+(?:\.\d+){1,3})(?:\s+#(\d+))?",
18
+ re.IGNORECASE,
19
+ )
20
+ ESXCLI_VERSION_RE = re.compile(r"^Version:\s*(\d+(?:\.\d+){1,3})\s*$", re.IGNORECASE | re.MULTILINE)
21
+ ESXCLI_BUILD_RE = re.compile(r"^Build:\s*(\d+)\s*$", re.IGNORECASE | re.MULTILINE)
22
+ ESXCLI_UPDATE_RE = re.compile(r"^Update:\s*(.+?)\s*$", re.IGNORECASE | re.MULTILINE)
23
+
24
+
25
+ def parse_esxi(text: str, data: dict[str, Any], p: OSData) -> OSData:
26
+ """Populate an OSData instance with ESXi-specific details."""
27
+ p.vendor = p.vendor or "VMware"
28
+ p.product = p.product or "VMware ESXi"
29
+ p.kernel_name = "vmkernel"
30
+
31
+ version: str | None = None
32
+ build: str | None = None
33
+ channel: str | None = None
34
+
35
+ prod_match = ESXI_PRODUCT_RE.search(text)
36
+ if prod_match:
37
+ version = prod_match.group(1)
38
+ build = prod_match.group(2) or build
39
+
40
+ kernel_match = VMKERNEL_RE.search(text)
41
+ if kernel_match:
42
+ version = version or kernel_match.group(1)
43
+ build = build or kernel_match.group(2)
44
+
45
+ version_line = ESXCLI_VERSION_RE.search(text)
46
+ if version_line and not version:
47
+ version = version_line.group(1)
48
+
49
+ build_line = ESXCLI_BUILD_RE.search(text)
50
+ if build_line and not build:
51
+ build = build_line.group(1)
52
+
53
+ update_line = ESXCLI_UPDATE_RE.search(text)
54
+ if update_line:
55
+ channel = update_line.group(1).strip()
56
+
57
+ if build:
58
+ p.version_build = build
59
+
60
+ if channel:
61
+ # Normalise "3" -> "Update 3", but keep existing text if already descriptive
62
+ if channel.isdigit():
63
+ p.channel = f"Update {channel}"
64
+ elif channel.lower().startswith("update"):
65
+ p.channel = channel
66
+ else:
67
+ p.channel = channel
68
+
69
+ if version:
70
+ p.kernel_version = version
71
+ major, minor, patch = parse_semver_like(version)
72
+ if major is not None:
73
+ p.version_major = major
74
+ if minor is not None:
75
+ p.version_minor = minor
76
+ if patch is not None:
77
+ p.version_patch = patch
78
+ p.precision = precision_from_parts(p.version_major, p.version_minor, p.version_patch, p.version_build)
79
+ else:
80
+ p.precision = PrecisionLevel.PRODUCT
81
+
82
+ update_confidence(p, p.precision)
83
+ return p
@@ -3,6 +3,7 @@
3
3
  import re
4
4
  from typing import Any, Optional
5
5
 
6
+ from os_normalizer.constants import PrecisionLevel
6
7
  from os_normalizer.helpers import parse_os_release, update_confidence
7
8
  from os_normalizer.models import OSData
8
9
 
@@ -31,7 +32,7 @@ def parse_linux(text: str, data: dict[str, Any], p: OSData) -> OSData:
31
32
  _apply_os_release(osrel, p)
32
33
  else:
33
34
  p.product = p.product or "Linux"
34
- p.precision = "family"
35
+ p.precision = PrecisionLevel.FAMILY
35
36
 
36
37
  update_confidence(p, p.precision)
37
38
  return p
@@ -92,13 +93,13 @@ def _apply_os_release(osrel: dict[str, Any], p: OSData) -> None:
92
93
 
93
94
  # Precision from version parts
94
95
  if p.version_patch is not None:
95
- p.precision = "patch"
96
+ p.precision = PrecisionLevel.PATCH
96
97
  elif p.version_minor is not None:
97
- p.precision = "minor"
98
+ p.precision = PrecisionLevel.MINOR
98
99
  elif p.version_major is not None:
99
- p.precision = "major"
100
+ p.precision = PrecisionLevel.MAJOR
100
101
  else:
101
- p.precision = "family"
102
+ p.precision = PrecisionLevel.FAMILY
102
103
 
103
104
 
104
105
  def _apply_version_id(vid: Any, p: OSData) -> None:
@@ -3,7 +3,7 @@
3
3
  import re
4
4
  from typing import Any
5
5
 
6
- from os_normalizer.constants import MACOS_ALIASES, MACOS_DARWIN_MAP
6
+ from os_normalizer.constants import MACOS_ALIASES, MACOS_DARWIN_MAP, PRECISION_ORDER, PrecisionLevel
7
7
  from os_normalizer.helpers import update_confidence
8
8
  from os_normalizer.models import OSData
9
9
 
@@ -14,10 +14,6 @@ DARWIN_RE = re.compile(
14
14
  )
15
15
  MACOS_VER_FALLBACK_RE = re.compile(r"\bmacos\s?(\d+)(?:\.(\d+))?", re.IGNORECASE)
16
16
 
17
- # Local precision order for simple comparisons
18
- _PRECISION_ORDER = {"family": 0, "product": 1, "major": 2, "minor": 3, "patch": 4, "build": 5}
19
-
20
-
21
17
  def parse_macos(text: str, data: dict[str, Any], p: OSData) -> OSData:
22
18
  """Populate an OSData instance with macOS-specific details."""
23
19
  t = text
@@ -50,7 +46,7 @@ def _apply_alias_hint(tl: str, p: OSData) -> None:
50
46
  parts = normalized.split()
51
47
  if len(parts) == 2 and parts[1].isdigit():
52
48
  p.version_major = int(parts[1])
53
- p.precision = _max_precision(p.precision, "major")
49
+ p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
54
50
 
55
51
 
56
52
  def _apply_darwin_mapping(t: str, p: OSData) -> None:
@@ -66,12 +62,12 @@ def _apply_darwin_mapping(t: str, p: OSData) -> None:
66
62
  p.product = prod
67
63
  if ver.isdigit():
68
64
  p.version_major = int(ver)
69
- p.precision = _max_precision(p.precision, "major")
65
+ p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
70
66
  else:
71
67
  x, y, *_ = ver.split(".")
72
68
  p.version_major = int(x)
73
69
  p.version_minor = int(y)
74
- p.precision = _max_precision(p.precision, "minor")
70
+ p.precision = _max_precision(p.precision, PrecisionLevel.MINOR)
75
71
  p.codename = code
76
72
 
77
73
 
@@ -84,9 +80,9 @@ def _apply_version_fallback(t: str, p: OSData) -> None:
84
80
  p.version_major = int(mm.group(1))
85
81
  if mm.group(2):
86
82
  p.version_minor = int(mm.group(2))
87
- p.precision = _max_precision(p.precision, "minor")
83
+ p.precision = _max_precision(p.precision, PrecisionLevel.MINOR)
88
84
  else:
89
- p.precision = _max_precision(p.precision, "major")
85
+ p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
90
86
 
91
87
 
92
88
  def _apply_codename_fallback(tl: str, p: OSData) -> None:
@@ -98,14 +94,16 @@ def _apply_codename_fallback(tl: str, p: OSData) -> None:
98
94
  # Provide at least major version from the map
99
95
  if isinstance(ver, str) and ver.isdigit():
100
96
  p.version_major = int(ver)
101
- p.precision = _max_precision(p.precision, "major")
97
+ p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
102
98
  elif isinstance(ver, str) and "." in ver:
103
99
  x, *_ = ver.split(".")
104
100
  if x.isdigit():
105
101
  p.version_major = int(x)
106
- p.precision = _max_precision(p.precision, "major")
102
+ p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
107
103
  break
108
104
 
109
105
 
110
- def _max_precision(current: str, new_label: str) -> str:
111
- return new_label if _PRECISION_ORDER.get(new_label, 0) > _PRECISION_ORDER.get(current, 0) else current
106
+ def _max_precision(current: PrecisionLevel, new_label: PrecisionLevel) -> PrecisionLevel:
107
+ if not isinstance(current, PrecisionLevel):
108
+ current = PrecisionLevel(current)
109
+ return new_label if PRECISION_ORDER.get(new_label, 0) > PRECISION_ORDER.get(current, 0) else current
@@ -1,7 +1,9 @@
1
1
  """Mobile device specific parsing logic."""
2
2
 
3
+ import re
3
4
  from typing import Any
4
5
 
6
+ from os_normalizer.constants import OSFamily, PrecisionLevel
5
7
  from os_normalizer.helpers import (
6
8
  parse_semver_like,
7
9
  precision_from_parts,
@@ -14,11 +16,21 @@ def parse_mobile(text: str, data: dict[str, Any], p: OSData) -> OSData:
14
16
  """Populate an OSData instance with mobile device-specific details."""
15
17
  t = text.lower()
16
18
 
19
+ # Detect if it's HarmonyOS before other mobile platforms to avoid vendor overlaps
20
+ if OSFamily.HARMONYOS.value in t:
21
+ p.product = "HarmonyOS"
22
+ p.vendor = "Huawei"
23
+ m = re.search(r"\b(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?\b", text)
24
+ if m:
25
+ p.version_major = int(m.group(1))
26
+ p.version_minor = int(m.group(2)) if m.group(2) else None
27
+ p.version_patch = int(m.group(3)) if m.group(3) else None
28
+ p.version_build = m.group(4) if m.group(4) else None
17
29
  # Detect if it's iOS or Android
18
- if "ios" in t or "ipados" in t:
30
+ elif OSFamily.IOS.value in t or "ipados" in t:
19
31
  p.product = "iOS/iPadOS"
20
32
  p.vendor = "Apple"
21
- elif "android" in t:
33
+ elif OSFamily.ANDROID.value in t:
22
34
  p.product = "Android"
23
35
  p.vendor = "Google"
24
36
  else:
@@ -27,9 +39,15 @@ def parse_mobile(text: str, data: dict[str, Any], p: OSData) -> OSData:
27
39
  p.vendor = None
28
40
 
29
41
  # Extract version info using semver-like parsing
30
- x, y, z = parse_semver_like(t)
31
- p.version_major, p.version_minor, p.version_patch = x, y, z
32
- p.precision = precision_from_parts(x, y, z, None) if x else "product"
42
+ if p.version_major is None:
43
+ x, y, z = parse_semver_like(t)
44
+ p.version_major, p.version_minor, p.version_patch = x, y, z
45
+
46
+ p.precision = (
47
+ precision_from_parts(p.version_major, p.version_minor, p.version_patch, p.version_build)
48
+ if p.version_major is not None
49
+ else PrecisionLevel.PRODUCT
50
+ )
33
51
 
34
52
  # Boost confidence based on precision
35
53
  update_confidence(p, p.precision)
@@ -15,6 +15,7 @@ from .fortinet import FORTI_RE, parse_fortinet
15
15
  from .huawei import HUAWEI_RE, parse_huawei
16
16
  from .netgear import NETGEAR_RE, parse_netgear
17
17
 
18
+ from os_normalizer.constants import OSFamily, PrecisionLevel
18
19
  from os_normalizer.models import OSData
19
20
 
20
21
  __all__ = [
@@ -57,5 +58,8 @@ def parse_network(text: str, data: dict | None, p: OSData) -> OSData:
57
58
  # Unknown network vendor; keep coarse
58
59
  p.vendor = p.vendor or "Unknown-Network"
59
60
  p.product = p.product or "Network OS"
60
- p.precision = "family"
61
+ if not isinstance(p.family, OSFamily):
62
+ p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
63
+ p.family = p.family or OSFamily.NETWORK
64
+ p.precision = PrecisionLevel.FAMILY
61
65
  return p
@@ -2,6 +2,7 @@
2
2
 
3
3
  import re
4
4
 
5
+ from os_normalizer.constants import CISCO_TRAIN_NAMES, OSFamily, PrecisionLevel
5
6
  from os_normalizer.helpers import update_confidence
6
7
  from os_normalizer.models import OSData
7
8
 
@@ -26,7 +27,9 @@ CISCO_EDITION_RE = re.compile(
26
27
 
27
28
  def parse_cisco(text: str, p: OSData) -> OSData:
28
29
  p.vendor = "Cisco"
29
- p.family = p.family or "network-os"
30
+ if not isinstance(p.family, OSFamily):
31
+ p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
32
+ p.family = p.family or OSFamily.NETWORK
30
33
 
31
34
  # Detect product line
32
35
  if CISCO_IOS_XE_RE.search(text):
@@ -53,14 +56,16 @@ def parse_cisco(text: str, p: OSData) -> OSData:
53
56
  p.version_patch = int(num[2])
54
57
  p.version_build = ver
55
58
  p.precision = (
56
- "patch" if p.version_patch is not None else ("minor" if p.version_minor is not None else "major")
59
+ PrecisionLevel.PATCH
60
+ if p.version_patch is not None
61
+ else (PrecisionLevel.MINOR if p.version_minor is not None else PrecisionLevel.MAJOR)
57
62
  )
58
63
 
59
64
  # Image filename
60
65
  img = CISCO_IMAGE_RE.search(text)
61
66
  if img:
62
67
  p.build_id = img.group(1)
63
- p.precision = "build"
68
+ p.precision = PrecisionLevel.BUILD
64
69
 
65
70
  # If NX-OS and only got version via filename, parse nxos.A.B.C.bin
66
71
  if not p.version_major and p.build_id:
@@ -70,7 +75,7 @@ def parse_cisco(text: str, p: OSData) -> OSData:
70
75
  p.version_minor = int(m.group(2))
71
76
  p.version_patch = int(m.group(3))
72
77
  p.version_build = f"{p.version_major}.{p.version_minor}.{p.version_patch}"
73
- p.precision = "patch"
78
+ p.precision = PrecisionLevel.PATCH
74
79
 
75
80
  # Model
76
81
  mm = CISCO_MODEL_RE.search(text)
@@ -83,8 +88,6 @@ def parse_cisco(text: str, p: OSData) -> OSData:
83
88
  p.edition = fl.group(1).lower()
84
89
 
85
90
  # Train codename
86
- from os_normalizer.constants import CISCO_TRAIN_NAMES
87
-
88
91
  tl = text.lower()
89
92
  for train in CISCO_TRAIN_NAMES:
90
93
  if train.lower() in tl:
@@ -92,5 +95,8 @@ def parse_cisco(text: str, p: OSData) -> OSData:
92
95
  break
93
96
 
94
97
  # Boost confidence based on precision
95
- update_confidence(p, p.precision if p.precision in ("build", "patch") else "minor")
98
+ update_confidence(
99
+ p,
100
+ p.precision if p.precision in (PrecisionLevel.BUILD, PrecisionLevel.PATCH) else PrecisionLevel.MINOR,
101
+ )
96
102
  return p
@@ -2,6 +2,7 @@
2
2
 
3
3
  import re
4
4
 
5
+ from os_normalizer.constants import OSFamily, PrecisionLevel
5
6
  from os_normalizer.helpers import update_confidence
6
7
  from os_normalizer.models import OSData
7
8
 
@@ -16,7 +17,9 @@ FORTI_CHANNEL_RE = re.compile(r"\((GA|Patch|Beta)\)", re.IGNORECASE)
16
17
  def parse_fortinet(text: str, p: OSData) -> OSData:
17
18
  p.vendor = "Fortinet"
18
19
  p.product = "FortiOS"
19
- p.family = p.family or "network-os"
20
+ if not isinstance(p.family, OSFamily):
21
+ p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
22
+ p.family = p.family or OSFamily.NETWORK
20
23
  p.kernel_name = "fortios"
21
24
 
22
25
  ver = FORTI_VER_RE.search(text)
@@ -30,17 +33,21 @@ def parse_fortinet(text: str, p: OSData) -> OSData:
30
33
  if len(nums) >= 3:
31
34
  p.version_patch = int(nums[2])
32
35
  p.version_build = v
33
- p.precision = "patch" if p.version_patch is not None else ("minor" if p.version_minor is not None else "major")
36
+ p.precision = (
37
+ PrecisionLevel.PATCH
38
+ if p.version_patch is not None
39
+ else (PrecisionLevel.MINOR if p.version_minor is not None else PrecisionLevel.MAJOR)
40
+ )
34
41
 
35
42
  bld = FORTI_BUILD_RE.search(text)
36
43
  if bld:
37
44
  p.version_build = (p.version_build or "") + f"+build.{bld.group(1)}"
38
- p.precision = "build"
45
+ p.precision = PrecisionLevel.BUILD
39
46
 
40
47
  img = FORTI_IMG_RE.search(text)
41
48
  if img:
42
49
  p.build_id = img.group(1)
43
- p.precision = "build"
50
+ p.precision = PrecisionLevel.BUILD
44
51
 
45
52
  mdl = FORTI_MODEL_RE.search(text)
46
53
  if mdl:
@@ -50,5 +57,8 @@ def parse_fortinet(text: str, p: OSData) -> OSData:
50
57
  if ch:
51
58
  p.channel = ch.group(1).upper()
52
59
 
53
- update_confidence(p, p.precision if p.precision in ("build", "patch") else "minor")
60
+ update_confidence(
61
+ p,
62
+ p.precision if p.precision in (PrecisionLevel.BUILD, PrecisionLevel.PATCH) else PrecisionLevel.MINOR,
63
+ )
54
64
  return p
@@ -2,6 +2,7 @@
2
2
 
3
3
  import re
4
4
 
5
+ from os_normalizer.constants import OSFamily, PrecisionLevel
5
6
  from os_normalizer.helpers import update_confidence
6
7
  from os_normalizer.models import OSData
7
8
 
@@ -14,7 +15,9 @@ HUAWEI_MODEL_RE = re.compile(r"\b(S\d{4}-\d{2}[A-Z-]+|CE\d{4}[A-Z-]*|AR\d{3,4}[A
14
15
  def parse_huawei(text: str, p: OSData) -> OSData:
15
16
  p.vendor = "Huawei"
16
17
  p.product = "VRP"
17
- p.family = p.family or "network-os"
18
+ if not isinstance(p.family, OSFamily):
19
+ p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
20
+ p.family = p.family or OSFamily.NETWORK
18
21
  p.kernel_name = "vrp"
19
22
 
20
23
  raw = HUAWEI_RAWVER_RE.search(text)
@@ -26,7 +29,7 @@ def parse_huawei(text: str, p: OSData) -> OSData:
26
29
  maj, r, _c = vm.group(1), vm.group(2), vm.group(3)
27
30
  p.version_major = int(maj)
28
31
  p.version_minor = int(r)
29
- p.precision = "minor"
32
+ p.precision = PrecisionLevel.MINOR
30
33
 
31
34
  mdl = HUAWEI_MODEL_RE.search(text)
32
35
  if mdl:
@@ -34,5 +37,8 @@ def parse_huawei(text: str, p: OSData) -> OSData:
34
37
 
35
38
  p.build_id = p.version_build or p.build_id
36
39
 
37
- update_confidence(p, p.precision if p.precision in ("minor", "build") else "major")
40
+ update_confidence(
41
+ p,
42
+ p.precision if p.precision in (PrecisionLevel.MINOR, PrecisionLevel.BUILD) else PrecisionLevel.MAJOR,
43
+ )
38
44
  return p
@@ -2,6 +2,7 @@
2
2
 
3
3
  import re
4
4
 
5
+ from os_normalizer.constants import OSFamily, PrecisionLevel
5
6
  from os_normalizer.helpers import update_confidence
6
7
  from os_normalizer.models import OSData
7
8
 
@@ -14,7 +15,9 @@ JUNOS_MODEL_RE = re.compile(r"\b(EX\d{3,4}-\d{2}[A-Z]?|QFX\d{3,4}\w*|SRX\d{3,4}\
14
15
  def parse_juniper(text: str, p: OSData) -> OSData:
15
16
  p.vendor = "Juniper"
16
17
  p.product = "Junos"
17
- p.family = p.family or "network-os"
18
+ if not isinstance(p.family, OSFamily):
19
+ p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
20
+ p.family = p.family or OSFamily.NETWORK
18
21
  p.kernel_name = "junos"
19
22
 
20
23
  vm = JUNOS_VER_RE.search(text)
@@ -27,16 +30,19 @@ def parse_juniper(text: str, p: OSData) -> OSData:
27
30
  if len(nums) >= 2:
28
31
  p.version_minor = int(nums[1])
29
32
  p.version_build = ver
30
- p.precision = "minor"
33
+ p.precision = PrecisionLevel.MINOR
31
34
 
32
35
  pkg = JUNOS_PKG_RE.search(text)
33
36
  if pkg:
34
37
  p.build_id = pkg.group(1)
35
- p.precision = "build"
38
+ p.precision = PrecisionLevel.BUILD
36
39
 
37
40
  mdl = JUNOS_MODEL_RE.search(text)
38
41
  if mdl:
39
42
  p.hw_model = mdl.group(1)
40
43
 
41
- update_confidence(p, p.precision if p.precision in ("build", "minor") else "major")
44
+ update_confidence(
45
+ p,
46
+ p.precision if p.precision in (PrecisionLevel.BUILD, PrecisionLevel.MINOR) else PrecisionLevel.MAJOR,
47
+ )
42
48
  return p
@@ -2,6 +2,7 @@
2
2
 
3
3
  import re
4
4
 
5
+ from os_normalizer.constants import OSFamily, PrecisionLevel
5
6
  from os_normalizer.helpers import update_confidence
6
7
  from os_normalizer.models import OSData
7
8
 
@@ -13,7 +14,9 @@ NETGEAR_MODEL_RE = re.compile(r"\b([RN][0-9]{3,4}[A-Z]?)\b", re.IGNORECASE)
13
14
  def parse_netgear(text: str, p: OSData) -> OSData:
14
15
  p.vendor = "Netgear"
15
16
  p.product = "Firmware"
16
- p.family = p.family or "network-os"
17
+ if not isinstance(p.family, OSFamily):
18
+ p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
19
+ p.family = p.family or OSFamily.NETWORK
17
20
  p.kernel_name = "firmware"
18
21
 
19
22
  vm = NETGEAR_VER_RE.search(text)
@@ -27,11 +30,15 @@ def parse_netgear(text: str, p: OSData) -> OSData:
27
30
  if len(nums) >= 3:
28
31
  p.version_patch = int(nums[2])
29
32
  p.version_build = v
30
- p.precision = "patch" if p.version_patch is not None else ("minor" if p.version_minor is not None else "major")
33
+ p.precision = (
34
+ PrecisionLevel.PATCH
35
+ if p.version_patch is not None
36
+ else (PrecisionLevel.MINOR if p.version_minor is not None else PrecisionLevel.MAJOR)
37
+ )
31
38
 
32
39
  mdl = NETGEAR_MODEL_RE.search(text)
33
40
  if mdl:
34
41
  p.hw_model = mdl.group(1)
35
42
 
36
- update_confidence(p, "minor" if p.precision == "major" else p.precision)
43
+ update_confidence(p, PrecisionLevel.MINOR if p.precision == PrecisionLevel.MAJOR else p.precision)
37
44
  return p
@@ -0,0 +1,100 @@
1
+ """Solaris specific parsing logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from os_normalizer.constants import PrecisionLevel
9
+ from os_normalizer.helpers import precision_from_parts, update_confidence
10
+ from os_normalizer.models import OSData
11
+
12
+ SUNOS_UNAME_RE = re.compile(
13
+ r"SunOS\s+\S+\s+(\d+(?:\.\d+)+)(?:\s+(\d+(?:\.\d+){1,4}))?",
14
+ re.IGNORECASE,
15
+ )
16
+ SOLARIS_RELEASE_RE = re.compile(
17
+ r"(?:Oracle\s+)?Solaris\s+(\d+(?:\.\d+){0,4})",
18
+ re.IGNORECASE,
19
+ )
20
+ GENERIC_BUILD_RE = re.compile(r"\bGeneric_(\S+)", re.IGNORECASE)
21
+
22
+
23
+ def parse_solaris(text: str, data: dict[str, Any], p: OSData) -> OSData:
24
+ """Populate an OSData instance with Solaris-specific details."""
25
+ # Baseline identity
26
+ p.vendor = p.vendor or "Oracle"
27
+ p.product = p.product or "Oracle Solaris"
28
+ p.kernel_name = "sunos"
29
+
30
+ kernel_version: str | None = None
31
+ release_version: str | None = None
32
+
33
+ # Extract version information from uname-style lines
34
+ uname_match = SUNOS_UNAME_RE.search(text)
35
+ if uname_match:
36
+ kernel_version = uname_match.group(1)
37
+ release_version = uname_match.group(2) or release_version
38
+
39
+ # /etc/release style information
40
+ release_match = SOLARIS_RELEASE_RE.search(text)
41
+ if release_match:
42
+ release_version = release_match.group(1)
43
+
44
+ # Normalise kernel version token if present
45
+ if kernel_version:
46
+ p.kernel_version = kernel_version
47
+
48
+ # Use the release token if available; fall back to kernel version
49
+ version_source = release_version or kernel_version
50
+
51
+ version_build: str | None = None
52
+ major: int | None = None
53
+ minor: int | None = None
54
+ patch: int | None = None
55
+
56
+ if version_source:
57
+ major, minor, patch, version_build = _split_solaris_version(version_source)
58
+
59
+ # Collect Generic_ build tags and prefer non-empty value
60
+ build_match = GENERIC_BUILD_RE.search(text)
61
+ if build_match:
62
+ version_build = version_build or build_match.group(1)
63
+
64
+ if major is not None:
65
+ p.version_major = major
66
+ if minor is not None:
67
+ p.version_minor = minor
68
+ if patch is not None:
69
+ p.version_patch = patch
70
+ if version_build:
71
+ p.version_build = version_build
72
+
73
+ if version_source:
74
+ p.precision = precision_from_parts(p.version_major, p.version_minor, p.version_patch, p.version_build)
75
+ else:
76
+ p.precision = PrecisionLevel.PRODUCT
77
+
78
+ update_confidence(p, p.precision)
79
+ return p
80
+
81
+
82
+ def _split_solaris_version(version: str) -> tuple[int | None, int | None, int | None, str | None]:
83
+ """Convert Solaris version tokens into (major, minor, patch, build)."""
84
+ parts = [int(token) for token in re.findall(r"\d+", version)]
85
+ if not parts:
86
+ return None, None, None, None
87
+
88
+ if parts[0] == 5 and len(parts) >= 2:
89
+ # SunOS 5.x maps to Solaris x
90
+ major = parts[1]
91
+ remainder = parts[2:]
92
+ else:
93
+ major = parts[0]
94
+ remainder = parts[1:]
95
+
96
+ minor = remainder[0] if len(remainder) >= 1 else None
97
+ patch = remainder[1] if len(remainder) >= 2 else None
98
+ extra = remainder[2:] if len(remainder) >= 3 else []
99
+ build = ".".join(str(x) for x in extra) if extra else None
100
+ return major, minor, patch, build
@@ -12,6 +12,7 @@ from os_normalizer.constants import (
12
12
  WINDOWS_NT_SERVER_MAP,
13
13
  WINDOWS_PRODUCT_PATTERNS,
14
14
  WINDOWS_SERVER_BUILD_MAP,
15
+ PrecisionLevel,
15
16
  )
16
17
  from os_normalizer.helpers import extract_arch_from_text, update_confidence
17
18
 
@@ -289,13 +290,13 @@ def _derive_precision(
289
290
  minor: int | None,
290
291
  patch: int | None,
291
292
  build: str | None,
292
- ) -> str:
293
+ ) -> PrecisionLevel:
293
294
  if build:
294
- return "build"
295
+ return PrecisionLevel.BUILD
295
296
  if patch is not None and patch != 0:
296
- return "patch"
297
+ return PrecisionLevel.PATCH
297
298
  if minor is not None:
298
- return "minor"
299
+ return PrecisionLevel.MINOR
299
300
  if major is not None:
300
- return "major"
301
- return "product"
301
+ return PrecisionLevel.MAJOR
302
+ return PrecisionLevel.PRODUCT
@@ -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