Coverage for utilities/release.py: 71%
59 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import datetime
2import importlib.util
3import os
4from dataclasses import asdict, dataclass, field
6import yaml
7from django.core.exceptions import ImproperlyConfigured
9from utilities.datetime import datetime_from_timestamp
11RELEASE_PATH = 'release.yaml'
12LOCAL_RELEASE_PATH = 'local/release.yaml'
15def _find_release_base_path():
16 """
17 Return the directory containing release.yaml.
19 In a source checkout, release.yaml lives under the NetBox application root
20 beside manage.py. In a wheel install, release.yaml is bundled under the
21 installed netbox package's _data directory.
22 """
23 checkout_base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24 if os.path.isfile(os.path.join(checkout_base_path, RELEASE_PATH)): 24 ↛ 27line 24 didn't jump to line 27 because the condition on line 24 was always true
25 return checkout_base_path
27 spec = importlib.util.find_spec('netbox') # pragma: no cover
28 locations = spec.submodule_search_locations if spec is not None else () # pragma: no cover
29 for location in locations or (): # pragma: no cover
30 bundled_base_path = os.path.join(location, '_data')
31 if os.path.isfile(os.path.join(bundled_base_path, RELEASE_PATH)):
32 return bundled_base_path
34 raise ImproperlyConfigured(f"Unable to locate {RELEASE_PATH} for this NetBox installation.") # pragma: no cover
37@dataclass
38class FeatureSet:
39 """
40 A map of all available NetBox features.
41 """
42 # Commercial support is provided by NetBox Labs
43 commercial: bool = False
45 # Live help center is enabled
46 help_center: bool = False
49@dataclass
50class ReleaseInfo:
51 version: str
52 edition: str
53 published: datetime.date | None = None
54 designation: str | None = None
55 build: str | None = None
56 features: FeatureSet = field(default_factory=FeatureSet)
58 @property
59 def full_version(self):
60 output = self.version
61 if self.designation: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true
62 output = f"{output}-{self.designation}"
63 if self.build: 63 ↛ 65line 63 didn't jump to line 65 because the condition on line 63 was always true
64 output = f"{output}-{self.build}"
65 return output
67 @property
68 def name(self):
69 return f"NetBox {self.edition} v{self.full_version}"
71 def asdict(self):
72 return asdict(self)
75def load_release_data():
76 """
77 Load any locally-defined release attributes and return a ReleaseInfo instance.
78 """
79 base_path = _find_release_base_path()
81 # Load canonical release attributes
82 with open(os.path.join(base_path, RELEASE_PATH)) as release_file:
83 data = yaml.safe_load(release_file)
85 # Overlay any local release date (if defined)
86 try:
87 with open(os.path.join(base_path, LOCAL_RELEASE_PATH)) as release_file:
88 local_data = yaml.safe_load(release_file)
89 except FileNotFoundError:
90 local_data = {}
91 if local_data is not None: 91 ↛ 99line 91 didn't jump to line 99 because the condition on line 91 was always true
92 if type(local_data) is not dict: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 raise ImproperlyConfigured(
94 f"{LOCAL_RELEASE_PATH}: Local release data must be defined as a dictionary."
95 )
96 data.update(local_data)
98 # Convert the published date to a date object
99 if 'published' in data: 99 ↛ 102line 99 didn't jump to line 102 because the condition on line 99 was always true
100 data['published'] = datetime_from_timestamp(data['published'])
102 return ReleaseInfo(**data)