Expose version as thing property and via thing state so it ends up in metadata.
This commit is contained in:
parent
1dd0db888d
commit
608c4bd16f
2 changed files with 101 additions and 6 deletions
|
|
@ -12,6 +12,8 @@ from uuid import UUID, uuid4
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
|
|
||||||
|
from openflexure_microscope_server.utilities import VersionData, robust_version_strings
|
||||||
|
|
||||||
|
|
||||||
class SettingsManager(lt.Thing):
|
class SettingsManager(lt.Thing):
|
||||||
"""Provides functionality to other Things about the current server state.
|
"""Provides functionality to other Things about the current server state.
|
||||||
|
|
@ -39,6 +41,23 @@ class SettingsManager(lt.Thing):
|
||||||
"""The hostname of the microscope, as reported by its operating system."""
|
"""The hostname of the microscope, as reported by its operating system."""
|
||||||
return gethostname()
|
return gethostname()
|
||||||
|
|
||||||
|
_version_data: Optional[VersionData] = None
|
||||||
|
|
||||||
|
@lt.thing_property
|
||||||
|
def version_data(self) -> VersionData:
|
||||||
|
"""The version string and version source for the server.
|
||||||
|
|
||||||
|
The source may be a commit hash if installed from git. "TOML" if installed from
|
||||||
|
source, or "Dist" if installed from a distribution package.
|
||||||
|
|
||||||
|
If the version or its source cannot be determined an error will be Logged.
|
||||||
|
"""
|
||||||
|
# Don't save as a setting as it may change. Get the version string the first
|
||||||
|
# time the property is requested this boot.
|
||||||
|
if self._version_data is None:
|
||||||
|
self._version_data = robust_version_strings()
|
||||||
|
return self._version_data
|
||||||
|
|
||||||
@lt.thing_action
|
@lt.thing_action
|
||||||
def get_things_state(self, metadata_getter: lt.deps.GetThingStates) -> Mapping:
|
def get_things_state(self, metadata_getter: lt.deps.GetThingStates) -> Mapping:
|
||||||
"""Metadata summarising the current state of all Things in the server."""
|
"""Metadata summarising the current state of all Things in the server."""
|
||||||
|
|
@ -50,4 +69,6 @@ class SettingsManager(lt.Thing):
|
||||||
return {
|
return {
|
||||||
"hostname": self.hostname,
|
"hostname": self.hostname,
|
||||||
"microscope-uuid": str(self.microscope_id),
|
"microscope-uuid": str(self.microscope_id),
|
||||||
|
"version": self.version_data.version,
|
||||||
|
"version_source": self.version_data.version_source,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import logging
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REPO_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
REPO_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
@ -89,7 +91,43 @@ def _wrap_and_catch_errors(target, error_buffer, *args, **kwargs):
|
||||||
error_buffer.append(e)
|
error_buffer.append(e)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionData(BaseModel):
|
||||||
|
"""A BaseModel containing the information about the server version.
|
||||||
|
|
||||||
|
:param version: The version string for the server. Or "Undefined" if there is an
|
||||||
|
error.
|
||||||
|
:param version_source: Either a Git commit hash, "TOML", "Dist" or in the case of
|
||||||
|
an error - "Undefined".
|
||||||
|
"""
|
||||||
|
|
||||||
|
version: str
|
||||||
|
version_source: str
|
||||||
|
|
||||||
|
|
||||||
def robust_version_strings() -> tuple[str, str]:
|
def robust_version_strings() -> tuple[str, str]:
|
||||||
|
"""Return a version string and information on its source.
|
||||||
|
|
||||||
|
Returning a python version string is not without problems. If the package is
|
||||||
|
installed in an editable way, often METDATA is never updated. This provides 4
|
||||||
|
ways to provide a version string:
|
||||||
|
|
||||||
|
1. If both a Git directory and a pyproject.toml are available. Return the version
|
||||||
|
string from the toml file and the git hash from the ``.git`` directory as its
|
||||||
|
source.
|
||||||
|
2. If a pyproject.toml is available but no Git directory then this is likely an
|
||||||
|
installation from source. Return the version string from the toml file and
|
||||||
|
return the literal string "TOML" as its source.
|
||||||
|
3. If a Git directory is available but no pyproject.toml then something is very
|
||||||
|
weird log an error. Then return "Undefined" as the version string, but still
|
||||||
|
return the Git hash as the source.
|
||||||
|
4. Finally if neither a Git directory nor a pyproject.toml are available then the
|
||||||
|
server has been installed from a distribution package (i.e. a wheel). In this
|
||||||
|
case use the standard library function ``importlib.metadata.version`` for the
|
||||||
|
version string (prepending a "v") and return "Dist" as its source.
|
||||||
|
|
||||||
|
:returns: a VersionData instance with the version string and source.
|
||||||
|
|
||||||
|
"""
|
||||||
project_toml_path = os.path.join(REPO_DIR, "pyproject.toml")
|
project_toml_path = os.path.join(REPO_DIR, "pyproject.toml")
|
||||||
git_dir_path = os.path.join(REPO_DIR, ".git")
|
git_dir_path = os.path.join(REPO_DIR, ".git")
|
||||||
has_project_toml = os.path.isfile(project_toml_path)
|
has_project_toml = os.path.isfile(project_toml_path)
|
||||||
|
|
@ -108,8 +146,8 @@ def robust_version_strings() -> tuple[str, str]:
|
||||||
source = "TOML"
|
source = "TOML"
|
||||||
elif has_git_dir:
|
elif has_git_dir:
|
||||||
# Only .git dir, that is weird.
|
# Only .git dir, that is weird.
|
||||||
LOGGER.warning(
|
LOGGER.error(
|
||||||
"Unexpected installation cofiguration. Version number cannot be verified."
|
"Unexpected installation configuration. Version number cannot be verified."
|
||||||
)
|
)
|
||||||
ver = "Undefined"
|
ver = "Undefined"
|
||||||
source = _get_hash_from_git_dir(git_dir_path)
|
source = _get_hash_from_git_dir(git_dir_path)
|
||||||
|
|
@ -119,10 +157,27 @@ def robust_version_strings() -> tuple[str, str]:
|
||||||
# This is why it is the last option.
|
# This is why it is the last option.
|
||||||
ver = "v" + version("openflexure_microscope_server")
|
ver = "v" + version("openflexure_microscope_server")
|
||||||
source = "Dist"
|
source = "Dist"
|
||||||
return (ver, source)
|
return VersionData(version=ver, version_source=source)
|
||||||
|
|
||||||
|
|
||||||
def _get_hash_from_git_dir(git_dir_path: str) -> str:
|
def _get_hash_from_git_dir(git_dir_path: str) -> str:
|
||||||
|
"""Get the commit hash from a .git directory directly.
|
||||||
|
|
||||||
|
This function doesn't rely on GIT being on the PATH or even installed. It doesn't
|
||||||
|
require any packages outside the Python standard library. It directly inspects the
|
||||||
|
.git directory to get the commit hash:
|
||||||
|
|
||||||
|
* If the repository is on a detached HEAD then the hash will be in the file
|
||||||
|
.git/HEAD. A detached HEAD is could be from checking out a tag or checking out
|
||||||
|
a commit directly.
|
||||||
|
* In normal operation .git/HEAD points to a reference or "ref". This ref file
|
||||||
|
contains the hash.
|
||||||
|
|
||||||
|
:param git_dir_path: The path to the .git directory.
|
||||||
|
|
||||||
|
:returns: The hash, or "Undefined" if there is any error, errors are logged not
|
||||||
|
raised.
|
||||||
|
"""
|
||||||
head_path = os.path.join(git_dir_path, "HEAD")
|
head_path = os.path.join(git_dir_path, "HEAD")
|
||||||
try:
|
try:
|
||||||
with open(head_path, "r", encoding="utf-8") as head_file:
|
with open(head_path, "r", encoding="utf-8") as head_file:
|
||||||
|
|
@ -141,8 +196,21 @@ def _get_hash_from_git_dir(git_dir_path: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _get_hash_from_git_ref(git_ref_path: str) -> str:
|
def _get_hash_from_git_ref(git_ref_path: str) -> str:
|
||||||
with open(git_ref_path, "r", encoding="utf-8") as ref_file:
|
"""Get the commit hash from a ref in the .git directory.
|
||||||
ref = ref_file.read()
|
|
||||||
|
For more detail see `_get_hash_from_git_dir`
|
||||||
|
|
||||||
|
:param git_ref_path: The full path to the ref file in the .git directory.
|
||||||
|
|
||||||
|
:returns: The hash, or "Undefined" if there is any error, errors are logged not
|
||||||
|
raised.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(git_ref_path, "r", encoding="utf-8") as ref_file:
|
||||||
|
ref = ref_file.read()
|
||||||
|
except IOError:
|
||||||
|
LOGGER.error("Problem opening Git branch reference.")
|
||||||
|
return "Undefined"
|
||||||
if ref_match := COMMIT_REGEX.match(ref):
|
if ref_match := COMMIT_REGEX.match(ref):
|
||||||
return ref_match.group(0)
|
return ref_match.group(0)
|
||||||
LOGGER.error("No hash found in Git ref")
|
LOGGER.error("No hash found in Git ref")
|
||||||
|
|
@ -150,10 +218,16 @@ def _get_hash_from_git_ref(git_ref_path: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _get_version_from_toml(toml_path: str) -> str:
|
def _get_version_from_toml(toml_path: str) -> str:
|
||||||
|
"""Get the version string from a pyproject.toml file.
|
||||||
|
|
||||||
|
:param toml_path: The path to the toml file.
|
||||||
|
|
||||||
|
:returns: The version string directly as reported in the toml file.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
with open(toml_path, "rb") as toml_file:
|
with open(toml_path, "rb") as toml_file:
|
||||||
toml_dict = tomllib.load(toml_file)
|
toml_dict = tomllib.load(toml_file)
|
||||||
return toml_dict["project"]["version"]
|
return toml_dict["project"]["version"]
|
||||||
except (IOError, ValueError, KeyError):
|
except (IOError, ValueError, KeyError):
|
||||||
LOGGER.error("Problem opening .git/HEAD")
|
LOGGER.error("Problem opening pyproject.toml")
|
||||||
return "Undefined"
|
return "Undefined"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue