Coverage for utilities/release.py: 71%

59 statements  

« 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 

5 

6import yaml 

7from django.core.exceptions import ImproperlyConfigured 

8 

9from utilities.datetime import datetime_from_timestamp 

10 

11RELEASE_PATH = 'release.yaml' 

12LOCAL_RELEASE_PATH = 'local/release.yaml' 

13 

14 

15def _find_release_base_path(): 

16 """ 

17 Return the directory containing release.yaml. 

18 

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 

26 

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 

33 

34 raise ImproperlyConfigured(f"Unable to locate {RELEASE_PATH} for this NetBox installation.") # pragma: no cover 

35 

36 

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 

44 

45 # Live help center is enabled 

46 help_center: bool = False 

47 

48 

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) 

57 

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 

66 

67 @property 

68 def name(self): 

69 return f"NetBox {self.edition} v{self.full_version}" 

70 

71 def asdict(self): 

72 return asdict(self) 

73 

74 

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() 

80 

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) 

84 

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) 

97 

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']) 

101 

102 return ReleaseInfo(**data)