Coverage for src/backend/InvenTree/InvenTree/version.py: 71%

153 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""Version information for InvenTree. 

2 

3Provides information on the current InvenTree version 

4""" 

5 

6import logging 

7import os 

8import pathlib 

9import platform 

10import re 

11import sys 

12from datetime import datetime as dt 

13from datetime import timedelta as td 

14 

15from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION 

16 

17# InvenTree software version 

18INVENTREE_SW_VERSION = '1.4.2' 

19 

20# Minimum supported Python version 

21MIN_PYTHON_VERSION = (3, 11) 

22 

23logger = logging.getLogger('inventree') 

24 

25git_warning_txt = 'INVE-W3: Could not detect git information.' 

26 

27# Discover git 

28try: 

29 from dulwich.errors import NotGitRepository 

30 from dulwich.porcelain import active_branch 

31 from dulwich.repo import Repo 

32 

33 try: 

34 main_repo = Repo(pathlib.Path(__file__).parent.parent.parent.parent.parent) 

35 main_commit = main_repo[main_repo.head()] 

36 except NotGitRepository: 

37 output = logger.warning 

38 

39 try: 

40 from django.conf import settings 

41 

42 if settings.DOCKER: 42 ↛ anywhereline 42 didn't jump anywhere: it always raised an exception.

43 output = logger.info 

44 except Exception: 

45 # We may not have access to settings at this point 

46 pass 

47 

48 output(git_warning_txt) 

49 

50 main_repo = None 

51 main_commit = None 

52 

53 try: 

54 main_branch = active_branch(main_repo) if main_repo else None 

55 except (KeyError, IndexError): 

56 logger.warning('INVE-W1: Current branch could not be detected.') 

57 main_branch = None 

58 

59 if main_repo is not None: 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true

60 main_repo.close() 

61 main_repo = None 

62except ImportError: 

63 logger.warning( 

64 'INVE-W2: Dulwich module not found, git information will not be available.' 

65 ) 

66 main_repo = None 

67 main_commit = None 

68 main_branch = None 

69except Exception as exc: 

70 logger.warning(git_warning_txt, exc_info=exc) 

71 main_repo = None 

72 main_commit = None 

73 main_branch = None 

74 

75 

76def checkMinPythonVersion(): 

77 """Check that the Python version meets the minimum requirements.""" 

78 V_MIN_MAJOR, V_MIN_MINOR = MIN_PYTHON_VERSION 

79 

80 version = sys.version.split(' ')[0] 

81 docs = 'https://docs.inventree.org/en/stable/start/#python-requirements' 

82 

83 msg = f""" 

84 INVE-E15: Python version not supported. 

85 InvenTree requires Python {V_MIN_MAJOR}.{V_MIN_MINOR} or above - you are running version {version}. 

86 - Refer to the InvenTree documentation for more information: 

87 - {docs} 

88 """ 

89 

90 if sys.version_info.major < V_MIN_MAJOR: 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true

91 raise RuntimeError(msg) 

92 

93 if sys.version_info.major == V_MIN_MAJOR and sys.version_info.minor < V_MIN_MINOR: 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true

94 raise RuntimeError(msg) 

95 

96 logger.info(f'Python version {version} - {sys.executable}') 

97 

98 

99def inventreeInstanceName() -> str: 

100 """Returns the InstanceName settings for the current database.""" 

101 from common.settings import get_global_setting 

102 

103 return get_global_setting('INVENTREE_INSTANCE') 

104 

105 

106def inventreeInstanceTitle() -> str: 

107 """Returns the InstanceTitle for the current database.""" 

108 from common.settings import get_global_setting 

109 

110 if get_global_setting('INVENTREE_INSTANCE_TITLE'): 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true

111 return get_global_setting('INVENTREE_INSTANCE') 

112 

113 return 'InvenTree' 

114 

115 

116def inventreeVersion() -> str: 

117 """Returns the InvenTree version string.""" 

118 return INVENTREE_SW_VERSION.lower().strip() 

119 

120 

121def inventreeVersionTuple(version=None): 

122 """Return the InvenTree version string as (maj, min, sub) tuple.""" 

123 if version is None: 

124 version = INVENTREE_SW_VERSION 

125 

126 match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', str(version)) 

127 

128 return [int(g) for g in match.groups()] if match else [] 

129 

130 

131def isInvenTreeDevelopmentVersion() -> bool: 

132 """Return True if current InvenTree version is a "development" version.""" 

133 return inventreeVersion().endswith('dev') 

134 

135 

136def inventreeDocUrl() -> str: 

137 """Return URL for InvenTree documentation site.""" 

138 return 'https://docs.inventree.org' 

139 

140 

141def inventreeAppUrl() -> str: 

142 """Return URL for InvenTree app site.""" 

143 return 'https://docs.inventree.org/en/latest/app/' 

144 

145 

146def inventreeGithubUrl() -> str: 

147 """Return URL for InvenTree github site.""" 

148 return 'https://github.com/InvenTree/InvenTree/' 

149 

150 

151def isInvenTreeUpToDate() -> bool: 

152 """Test if the InvenTree instance is "up to date" with the latest version. 

153 

154 A background task periodically queries GitHub for latest version, and stores it to the database as "_INVENTREE_LATEST_VERSION" 

155 """ 

156 from common.settings import get_global_setting 

157 

158 latest = get_global_setting( 

159 '_INVENTREE_LATEST_VERSION', backup_value=None, create=False 

160 ) 

161 

162 # No record for "latest" version - we must assume we are up to date! 

163 if not latest: 163 ↛ 167line 163 didn't jump to line 167 because the condition on line 163 was always true

164 return True 

165 

166 # Extract "tuple" version (Python can directly compare version tuples) 

167 latest_version = inventreeVersionTuple(latest) # pragma: no cover 

168 inventree_version = inventreeVersionTuple() # pragma: no cover 

169 

170 return inventree_version >= latest_version # pragma: no cover 

171 

172 

173def inventreeApiVersion() -> int: 

174 """Returns current API version of InvenTree.""" 

175 return INVENTREE_API_VERSION 

176 

177 

178def parse_version_text(): 

179 """Parse the version text to structured data.""" 

180 patched_data = INVENTREE_API_TEXT.split('\n\n') 

181 # Remove first newline on latest version 

182 patched_data[0] = patched_data[0].replace('\n', '', 1) 

183 

184 latest_version = f'v{INVENTREE_API_VERSION}' 

185 version_data = {} 

186 for version in patched_data: 

187 data = version.split('\n') 

188 

189 version_split = data[0].split(' -> ') 

190 version_string = version_split[0].strip() 

191 if version_string == '': 

192 continue 

193 

194 version_detail = ( 

195 version_split[1].split(':', 1) if len(version_split) > 1 else [''] 

196 ) 

197 new_data = { 

198 'version': version_string, 

199 'date': version_detail[0].strip(), 

200 'gh': version_detail[1].strip() if len(version_detail) > 1 else None, 

201 'text': data[1:], 

202 'latest': latest_version == version_string, 

203 } 

204 version_data[version_string] = new_data 

205 return version_data 

206 

207 

208INVENTREE_API_TEXT_DATA = parse_version_text() 

209"""Pre-processed API version text.""" 

210 

211 

212def inventreeApiText(versions: int = 10, start_version: int = 0): 

213 """Returns API version descriptors. 

214 

215 Args: 

216 versions: Number of versions to return. Default: 10 

217 start_version: first version to report. Defaults to return the latest {versions} versions. 

218 """ 

219 version_data = INVENTREE_API_TEXT_DATA 

220 

221 # Define the range of versions to return 

222 if start_version == 0: 

223 start_version = INVENTREE_API_VERSION - versions + 1 

224 

225 return { 

226 f'v{a}': version_data.get(f'v{a}', None) 

227 for a in range(start_version, start_version + versions) 

228 } 

229 

230 

231def inventreeDjangoVersion(): 

232 """Returns the version of Django library.""" 

233 import django 

234 

235 return django.get_version() 

236 

237 

238def inventreePythonVersion(): 

239 """Returns the version of python.""" 

240 return sys.version.split(' ')[0] 

241 

242 

243def inventreeCommitHash(): 

244 """Returns the git commit hash for the running codebase.""" 

245 # First look in the environment variables, i.e. if running in docker 

246 commit_hash = os.environ.get('INVENTREE_COMMIT_HASH', '') 

247 

248 if commit_hash: 248 ↛ 251line 248 didn't jump to line 251 because the condition on line 248 was always true

249 return commit_hash 

250 

251 if main_commit is None: 

252 return None 

253 return main_commit.sha().hexdigest()[0:7] 

254 

255 

256def inventreeCommitDate(): 

257 """Returns the git commit date for the running codebase.""" 

258 # First look in the environment variables, e.g. if running in docker 

259 commit_date = os.environ.get('INVENTREE_COMMIT_DATE', '') 

260 

261 if commit_date: 261 ↛ 264line 261 didn't jump to line 264 because the condition on line 261 was always true

262 return commit_date.split(' ')[0] 

263 

264 if main_commit is None: 

265 return None 

266 

267 commit_dt = dt.fromtimestamp(main_commit.commit_time) + td( 

268 seconds=main_commit.commit_timezone 

269 ) 

270 return str(commit_dt.date()) 

271 

272 

273def inventreeBranch(): 

274 """Returns the branch for the running codebase - if set.""" 

275 # First look in the environment variables, e.g. if running in docker 

276 

277 branch = os.environ.get('INVENTREE_PKG_BRANCH', '') 

278 

279 if branch: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true

280 return ' '.join(branch.splitlines()) 

281 

282 if main_branch is None: 282 ↛ 284line 282 didn't jump to line 284 because the condition on line 282 was always true

283 return None # pragma: no cover - branch information may not be available in all environments 

284 return main_branch.decode( 

285 'utf-8' 

286 ) # pragma: no cover - branch information may not be available in all environments 

287 

288 

289def inventreeTarget(): 

290 """Returns the target platform for the running codebase - if set.""" 

291 # First look in the environment variables, e.g. if running in docker 

292 

293 return os.environ.get('INVENTREE_PKG_TARGET', None) 

294 

295 

296def inventreePlatform(): 

297 """Returns the platform for the instance.""" 

298 return platform.platform(aliased=True) 

299 

300 

301def inventreeDatabase(): 

302 """Return the InvenTree database backend e.g. 'postgresql'.""" 

303 from django.conf import settings 

304 

305 return settings.DB_ENGINE 

306 

307 

308def inventree_identifier(override_announce: bool = False): 

309 """Return the InvenTree instance ID.""" 

310 from common.settings import get_global_setting 

311 

312 if override_announce or get_global_setting( 312 ↛ 315line 312 didn't jump to line 315 because the condition on line 312 was never true

313 'INVENTREE_ANNOUNCE_ID', environment_key='INVENTREE_ANNOUNCE_ID' 

314 ): 

315 return get_global_setting('INVENTREE_INSTANCE_ID', default='') 

316 return None