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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Version information for InvenTree.
3Provides information on the current InvenTree version
4"""
6import logging
7import os
8import pathlib
9import platform
10import re
11import sys
12from datetime import datetime as dt
13from datetime import timedelta as td
15from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
17# InvenTree software version
18INVENTREE_SW_VERSION = '1.4.2'
20# Minimum supported Python version
21MIN_PYTHON_VERSION = (3, 11)
23logger = logging.getLogger('inventree')
25git_warning_txt = 'INVE-W3: Could not detect git information.'
27# Discover git
28try:
29 from dulwich.errors import NotGitRepository
30 from dulwich.porcelain import active_branch
31 from dulwich.repo import Repo
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
39 try:
40 from django.conf import settings
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
48 output(git_warning_txt)
50 main_repo = None
51 main_commit = None
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
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
76def checkMinPythonVersion():
77 """Check that the Python version meets the minimum requirements."""
78 V_MIN_MAJOR, V_MIN_MINOR = MIN_PYTHON_VERSION
80 version = sys.version.split(' ')[0]
81 docs = 'https://docs.inventree.org/en/stable/start/#python-requirements'
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 """
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)
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)
96 logger.info(f'Python version {version} - {sys.executable}')
99def inventreeInstanceName() -> str:
100 """Returns the InstanceName settings for the current database."""
101 from common.settings import get_global_setting
103 return get_global_setting('INVENTREE_INSTANCE')
106def inventreeInstanceTitle() -> str:
107 """Returns the InstanceTitle for the current database."""
108 from common.settings import get_global_setting
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')
113 return 'InvenTree'
116def inventreeVersion() -> str:
117 """Returns the InvenTree version string."""
118 return INVENTREE_SW_VERSION.lower().strip()
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
126 match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', str(version))
128 return [int(g) for g in match.groups()] if match else []
131def isInvenTreeDevelopmentVersion() -> bool:
132 """Return True if current InvenTree version is a "development" version."""
133 return inventreeVersion().endswith('dev')
136def inventreeDocUrl() -> str:
137 """Return URL for InvenTree documentation site."""
138 return 'https://docs.inventree.org'
141def inventreeAppUrl() -> str:
142 """Return URL for InvenTree app site."""
143 return 'https://docs.inventree.org/en/latest/app/'
146def inventreeGithubUrl() -> str:
147 """Return URL for InvenTree github site."""
148 return 'https://github.com/InvenTree/InvenTree/'
151def isInvenTreeUpToDate() -> bool:
152 """Test if the InvenTree instance is "up to date" with the latest version.
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
158 latest = get_global_setting(
159 '_INVENTREE_LATEST_VERSION', backup_value=None, create=False
160 )
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
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
170 return inventree_version >= latest_version # pragma: no cover
173def inventreeApiVersion() -> int:
174 """Returns current API version of InvenTree."""
175 return INVENTREE_API_VERSION
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)
184 latest_version = f'v{INVENTREE_API_VERSION}'
185 version_data = {}
186 for version in patched_data:
187 data = version.split('\n')
189 version_split = data[0].split(' -> ')
190 version_string = version_split[0].strip()
191 if version_string == '':
192 continue
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
208INVENTREE_API_TEXT_DATA = parse_version_text()
209"""Pre-processed API version text."""
212def inventreeApiText(versions: int = 10, start_version: int = 0):
213 """Returns API version descriptors.
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
221 # Define the range of versions to return
222 if start_version == 0:
223 start_version = INVENTREE_API_VERSION - versions + 1
225 return {
226 f'v{a}': version_data.get(f'v{a}', None)
227 for a in range(start_version, start_version + versions)
228 }
231def inventreeDjangoVersion():
232 """Returns the version of Django library."""
233 import django
235 return django.get_version()
238def inventreePythonVersion():
239 """Returns the version of python."""
240 return sys.version.split(' ')[0]
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', '')
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
251 if main_commit is None:
252 return None
253 return main_commit.sha().hexdigest()[0:7]
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', '')
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]
264 if main_commit is None:
265 return None
267 commit_dt = dt.fromtimestamp(main_commit.commit_time) + td(
268 seconds=main_commit.commit_timezone
269 )
270 return str(commit_dt.date())
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
277 branch = os.environ.get('INVENTREE_PKG_BRANCH', '')
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())
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
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
293 return os.environ.get('INVENTREE_PKG_TARGET', None)
296def inventreePlatform():
297 """Returns the platform for the instance."""
298 return platform.platform(aliased=True)
301def inventreeDatabase():
302 """Return the InvenTree database backend e.g. 'postgresql'."""
303 from django.conf import settings
305 return settings.DB_ENGINE
308def inventree_identifier(override_announce: bool = False):
309 """Return the InvenTree instance ID."""
310 from common.settings import get_global_setting
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