Source code for qblox_instruments.build

# --------------------------------------------------------------------------
# Description    : Qblox instruments build information
# Git repository : https://gitlab.com/qblox/packages/software/qblox_instruments.git
# Copyright (C) Qblox BV (2020)
# --------------------------------------------------------------------------


# -- include -----------------------------------------------------------------

import re
import functools

from datetime import datetime
from typing import Union, Optional, Tuple


# -- definitions -------------------------------------------------------------

# Wildcard import definition
__all__ = ["get_build_info", "BuildInfo", "DeviceInfo", "__version__"]


# -- classes -----------------------------------------------------------------


[docs] @functools.total_ordering class BuildInfo: """ Class representing build information for a particular component. """ __slots__ = ["_version", "_build", "_hash", "_dirty"] # ------------------------------------------------------------------------
[docs] def __init__( self, version: Union[str, Tuple[int, int, int]], build: Union[str, int, datetime], hash: Union[str, int], dirty: Union[str, bool], ): """ Makes a build information object. Parameters ---------- version: Union[str, Tuple[int, int, int]] Either a canonical version string or a three-tuple of integers. build: Union[str, int, datetime], The build timestamp, either as a string formatted like "17/11/2021-19:04:53" (as used in ``*IDN?``), a Unix timestamp in seconds, or a Python datetime object. hash: Union[str, int] The git hash of the repository that the build was run from, either as a hex string with at least 8 characters, or as an integer. If 0x is prefixed, the hash may have less than 8 digits, implying zeros in front. dirty: Union[str, bool] Whether the git repository was dirty at the time of the build, either as a ``0`` or ``1`` string (as in ``*IDN?``) or as the boolean itself. """ # Convert and check version. if isinstance(version, str): version = map(int, version.split(".")) version = tuple(version) if len(version) != 3: raise ValueError("invalid version specified") for comp in version: if not isinstance(comp, int): raise TypeError("unsupported type for version") if comp < 0: raise ValueError("invalid version specified") self._version = version # Convert and check build timestamp. if isinstance(build, str): build = datetime.strptime(build, "%d/%m/%Y-%H:%M:%S") elif isinstance(build, int): build = datetime.fromtimestamp(build) if not isinstance(build, datetime): raise TypeError("unsupported type for build") self._build = build # Convert and check git hash. if isinstance(hash, str): m = re.fullmatch("0x[0-9a-fA-F]{1,8}|[0-9a-fA-F]{8}", hash) if not m: raise ValueError(f"invalid or too short git hash specified: {hash!r}") hash = int(m.group(0), 16) if not isinstance(hash, int): raise TypeError("unsupported type for hash") if hash < 0 or hash > 0xFFFFFFFF: raise ValueError("hash integer out of range") self._hash = hash # Convert and check dirty flag. if isinstance(dirty, str): if dirty == "0": dirty = False elif dirty == "1": dirty = True else: raise ValueError("invalid string specified for dirty") if not isinstance(dirty, bool): raise TypeError("unsupported type for dirty") self._dirty = dirty
# ------------------------------------------------------------------------ @property def version(self) -> Tuple[int, int, int]: """ The version as a three-tuple. :type: Tuple[int, int, int] """ return self._version # ------------------------------------------------------------------------ @property def version_str(self) -> str: """ The version as a string. :type: str """ return ".".join(map(str, self._version)) # ------------------------------------------------------------------------ @property def build(self) -> datetime: """ The build timestamp as a datetime object. :type: datetime """ return self._build # ------------------------------------------------------------------------ @property def build_str(self) -> str: """ The build time as a string, as formatted for ``*IDN?``. :type: str """ return self._build.strftime("%d/%m/%Y-%H:%M:%S") # ------------------------------------------------------------------------ @property def build_iso(self) -> str: """ The build time as a string, formatted using the ISO date format. :type: str """ return self._build.isoformat() # ------------------------------------------------------------------------ @property def build_unix(self) -> int: """ The build time as a unix timestamp in seconds. :type: int """ return int(self._build.timestamp()) # ------------------------------------------------------------------------ @property def hash(self) -> int: """ The git hash as an integer. :type: int """ return int(self._hash) # ------------------------------------------------------------------------ @property def hash_str(self) -> str: """ The git hash as a string. :type: str """ return f"{self._hash:08x}" # ------------------------------------------------------------------------ @property def dirty(self) -> bool: """ Whether the repository was dirty during the build. :type: bool """ return self._dirty # ------------------------------------------------------------------------ @property def dirty_str(self) -> str: """ The dirty flag as a ``0`` or ``1`` string (as used for ``*IDN?``). :type: str """ return "1" if self._dirty else "0" # ------------------------------------------------------------------------
[docs] @classmethod def from_idn(cls, idn: str, prefix: str = "") -> Optional["BuildInfo"]: """ Constructs a build information structure from an ``*IDN?`` string. Parameters ---------- idn: str The ``*IDN?`` string. prefix: str The prefix used for each key (currently ``fw``, ``kmod``, ``sw``, or ``cfgMan``). Returns ------- Optional[BuildInfo] The build information structure if data is available for the given key, or None if not. """ build_data = { x[0]: x[1] for x in (s.split("=", maxsplit=1) for s in idn.split(",")[-1].split()) } try: return cls( build_data[f"{prefix}Version"], build_data[f"{prefix}Build"], build_data[f"{prefix}Hash"], build_data[f"{prefix}Dirty"], ) except KeyError: return None
# ------------------------------------------------------------------------
[docs] def to_idn(self, prefix: str = "") -> str: """ Formats this build information object in the same way ``*IDN?`` is formatted. Parameters ---------- prefix: str The prefix used for each key (currently ``fw``, ``kmod``, ``sw``, or ``cfgMan``). Returns ------- str The part of the ``*IDN?`` string for this build information object. """ return "{4}Version={0} {4}Build={1} {4}Hash=0x{2:08X} {4}Dirty={3}".format( self.version_str, self.build_str, self.hash, self.dirty_str, prefix )
# ------------------------------------------------------------------------
[docs] @classmethod def from_dict(cls, build_data: dict) -> "BuildInfo": """ Constructs a build information structure from a JSON-capable dict, as used in ZeroMQ/CBOR descriptions, plug&play descriptions, update file metadata, and various other places. Parameters ---------- build_data: dict Dictionary with (at least) the following keys: - ``"version"``: iterable of three integers representing the version; - ``"build"``: Unix timestamp in seconds representing the build timestamp; - ``"hash"``: the first 8 hex digits of the git hash as an integer; and - ``"dirty"``: boolean dirty flag. Returns ------- BuildInfo The build information structure. """ return cls( build_data["version"], build_data["build"], build_data["hash"], build_data["dirty"], )
# ------------------------------------------------------------------------
[docs] def to_dict(self) -> dict: """ Formats this build information object as a JSON-capable dict, as used in ZeroMQ/CBOR descriptions, plug&play descriptions, update file metadata, and various other places. Parameters ---------- None Returns ------- dict The generated dictionary, having the following keys: - ``"version"``: iterable of three integers representing the version; - ``"build"``: Unix timestamp in seconds representing the build timestamp; - ``"hash"``: the first 8 hex digits of the git hash as an integer; and - ``"dirty"``: boolean dirty flag. """ return { "version": self.version, "build": self.build_unix, "hash": self.hash, "dirty": self.dirty, }
# ------------------------------------------------------------------------
[docs] def to_idn_dict(self) -> dict: """ Formats this build information object as a human-readable JSON-capable dict, as used in get_idn. Parameters ---------- Returns ------- dict The generated dictionary, having the following keys: - ``"version"``: string representation of the version; - ``"build"``: string representation of timestamp in seconds representing the build timestamp; - ``"hash"``: string representation of the first 8 hex digits of the git hash; and - ``"dirty"``: boolean dirty flag. """ return { "version": self.version_str, "build": self.build_str, "hash": self.hash_str, "dirty": self.dirty, }
# ------------------------------------------------------------------------
[docs] def to_tuple(self) -> tuple: """ Formats this build information object as a tuple for ordering purposes. Parameters ---------- None Returns ------- tuple A tuple, containing all the information in this structure in a canonical format. """ return (self.version, self.build_unix, self.hash, self.dirty)
# ------------------------------------------------------------------------ def __eq__(self, other) -> bool: if isinstance(other, BuildInfo): return self.to_tuple() == other.to_tuple() return NotImplemented # ------------------------------------------------------------------------ def __lt__(self, other) -> bool: if isinstance(other, BuildInfo): return self.to_tuple() < other.to_tuple() return NotImplemented # ------------------------------------------------------------------------ def __str__(self) -> str: return f"{self.version_str}, built on {self.build_iso} from git hash {self.hash_str}{' (dirty)' if self.dirty else ''}"
# --------------------------------------------------------------------------
[docs] class DeviceInfo: """ Class representing the build and model information of a device. Has the same information content as what ``*IDN?`` returns. """ __slots__ = [ "_manufacturer", "_model", "_serial", "_sw_build", "_fw_build", "_kmod_build", "_cfg_man_build", ] # ------------------------------------------------------------------------
[docs] def __init__( self, manufacturer: str, model: str, serial: Optional[str] = None, sw_build: Optional[BuildInfo] = None, fw_build: Optional[BuildInfo] = None, kmod_build: Optional[BuildInfo] = None, cfg_man_build: Optional[BuildInfo] = None, ): if not isinstance(manufacturer, str): raise TypeError("invalid type specified for manufacturer") self._manufacturer = manufacturer.replace(" ", "_").lower() if not isinstance(model, str): raise TypeError("invalid type specified for model") self._model = model.replace(" ", "_").lower() if serial is not None and not isinstance(serial, str): raise TypeError("invalid type specified for serial") self._serial = serial if sw_build is not None and not isinstance(sw_build, BuildInfo): raise TypeError("invalid type specified for sw_build") self._sw_build = sw_build if fw_build is not None and not isinstance(fw_build, BuildInfo): raise TypeError("invalid type specified for fw_build") self._fw_build = fw_build if kmod_build is not None and not isinstance(kmod_build, BuildInfo): raise TypeError("invalid type specified for kmod_build") self._kmod_build = kmod_build if cfg_man_build is not None and not isinstance(cfg_man_build, BuildInfo): raise TypeError("invalid type specified for cfg_man_build") self._cfg_man_build = cfg_man_build
# ------------------------------------------------------------------------ @property def manufacturer(self) -> str: """ The manufacturer name, in lowercase_with_underscores format. :type: str """ return self._manufacturer # ------------------------------------------------------------------------ @property def model(self) -> str: """ The model name, in lowercase_with_underscores format. :type: str """ return self._model device = model # ------------------------------------------------------------------------ @property def serial(self) -> Optional[str]: """ The serial number, if known. :type: Optional[str] """ return self._serial # ------------------------------------------------------------------------ @property def sw_build(self) -> Optional[BuildInfo]: """ The software/application build information, if known. :type: Optional[BuildInfo] """ return self._sw_build # ------------------------------------------------------------------------ @property def fw_build(self) -> Optional[BuildInfo]: """ The FPGA firmware build information, if known. :type: Optional[BuildInfo] """ return self._fw_build # ------------------------------------------------------------------------ @property def kmod_build(self) -> Optional[BuildInfo]: """ The kernel module build information, if known. :type: Optional[BuildInfo] """ return self._kmod_build # ------------------------------------------------------------------------ @property def cfg_man_build(self) -> Optional[BuildInfo]: """ The configuration management build information, if known. :type: Optional[BuildInfo] """ return self._cfg_man_build # ------------------------------------------------------------------------
[docs] def get_build_info(self, key: str) -> Optional[BuildInfo]: """ Returns build information for the given key. Parameters ---------- key: str The key. Must be one of: - ``"sw"``: returns the application build info; - ``"fw"``: returns the FPGA firmware build info; - ``"kmod"``: returns the kernel module build info; or - ``"cfg_man"`` or ``"cfgMan"``: returns the configuration manager build info. Returns ------- Optional[BuildInfo] The build information structure, if known. Raises ------ KeyError For unknown keys. """ if key == "sw": return self._sw_build elif key == "fw": return self._fw_build elif key == "kmod": return self._kmod_build elif key in ("cfg_man", "cfgMan"): return self._cfg_man_build else: raise KeyError(f"unknown key {key!r}")
# ------------------------------------------------------------------------ def __getitem__(self, key: str) -> BuildInfo: """ Same as get_build_info(), but raises a KeyError if no data is known. Parameters ---------- key: str The key. Must be one of: - ``"sw"``: returns the application build info; - ``"fw"``: returns the FPGA firmware build info; - ``"kmod"``: returns the kernel module build info; or - ``"cfg_man"`` or ``"cfgMan"``: returns the configuration manager build info. Returns ------- BuildInfo The build information structure. Raises ------ KeyError If no data is known for the given key or the key itself is unknown. """ result = self.get_build_info(key) if result is None: raise KeyError(f"no data for key {key!r}") return result # ------------------------------------------------------------------------ def __contains__(self, key: str) -> bool: """ Returns whether data is known for the given key. Parameters ---------- key: str The key. Must be one of: - ``"sw"``: returns the application build info; - ``"fw"``: returns the FPGA firmware build info; - ``"kmod"``: returns the kernel module build info; or - ``"cfg_man"`` or ``"cfgMan"``: returns the configuration manager build info. Returns ------- bool Whether data is known. """ try: return self.get_build_info(key) is not None except KeyError: return False # ------------------------------------------------------------------------
[docs] @classmethod def from_idn(cls, idn: str) -> "DeviceInfo": """ Constructs a device information structure from an ``*IDN?`` string. Parameters ---------- idn: str The ``*IDN?`` string. Returns ------- DeviceInfo The parsed device information structure. """ manufacturer, model, *serial, build_data = idn.split(",") if serial: serial = serial[0] else: serial = None return cls( manufacturer, model, serial, BuildInfo.from_idn(build_data, "sw"), BuildInfo.from_idn(build_data, "fw"), BuildInfo.from_idn(build_data, "kmod"), BuildInfo.from_idn(build_data, "cfgMan"), )
# ------------------------------------------------------------------------
[docs] def to_idn(self) -> str: """ Formats this device information object in the same way ``*IDN?`` is formatted. Parameters ---------- Returns ------- str The ``*IDN?`` string. """ idn = [] for key in ("sw", "fw", "kmod", "cfgMan"): bi = self.get_build_info(key) if bi is not None: idn.append(bi.to_idn(key)) idn = " ".join(idn) if self._serial is not None: idn = f"{self._serial},{idn}" idn = f"{self._manufacturer},{self._model},{idn}" return idn
# ------------------------------------------------------------------------
[docs] @classmethod def from_dict(cls, description: dict) -> "DeviceInfo": """ Constructs a device information structure from a JSON-capable dict, as used in ZeroMQ/CBOR descriptions, plug&play descriptions, update file metadata, and various other places. Parameters ---------- description: dict Dictionary with the following keys: - ``"manufacturer"``: manufacturer name (string); - ``"model"``: model name (string); - ``"ser"``: serial number (string); - ``"sw"``: application build information (dict); - ``"fw"``: FPGA firmware build information (dict); - ``"kmod"``: kernel module build information (dict); and - ``"cfg_man"``: configuration management build information (dict); Returns ------- DeviceInfo The build information structure. """ return cls( description.get("manufacturer", "unknown"), description.get("model", "unknown"), description.get("ser", None), BuildInfo.from_dict(description["sw"]) if "sw" in description else None, BuildInfo.from_dict(description["fw"]) if "fw" in description else None, BuildInfo.from_dict(description["kmod"]) if "kmod" in description else None, ( BuildInfo.from_dict(description["cfg_man"]) if "cfg_man" in description else None ), )
# ------------------------------------------------------------------------
[docs] def to_dict(self) -> dict: """ Formats this device information object as a JSON-capable dict, as used in ZeroMQ/CBOR descriptions, plug&play descriptions, update file metadata, and various other places. Parameters ---------- Returns ------- dict The generated dictionary, having the following keys: - ``"manufacturer"``: manufacturer name (string); - ``"model"``: model name (string); - ``"ser"``: serial number (string); - ``"sw"``: application build information (dict); - ``"fw"``: FPGA firmware build information (dict); - ``"kmod"``: kernel module build information (dict); and/or - ``"cfg_man"``: configuration management build information (dict); Some keys may be omitted if the information is not available. """ description = {} if self._manufacturer != "unknown": description["manufacturer"] = self._manufacturer if self._model != "unknown": description["model"] = self._model if self._serial is not None: description["ser"] = self._serial for key in ("sw", "fw", "kmod", "cfg_man"): bi = self.get_build_info(key) if bi is not None: description[key] = bi.to_dict() return description
# ------------------------------------------------------------------------
[docs] def to_idn_dict(self) -> dict: """ Formats this device information object as a human-readable JSON-capable dict, as used get_idn. Parameters ---------- Returns ------- dict The generated dictionary, having the following keys: - ``"manufacturer"``: manufacturer name (string); - ``"model"``: model name (string); - ``"serial_number"``: serial number (string); - ``"firmware"``: build info (dict); - ``"fpga"``: FPGA firmware build information (dict); - ``"kernel_mod"``: kernel module build information (dict); - ``"application"``: application build information (dict); and - ``"driver"``: driver build information (dict); Some keys may be omitted if the information is not available. """ description = {} if self._manufacturer != "unknown": description["manufacturer"] = self._manufacturer if self._model != "unknown": description["model"] = self._model if self._serial is not None: description["serial_number"] = self._serial description["firmware"] = {} for key, idn_key in zip( ["fw", "kmod", "sw"], ["fpga", "kernel_mod", "application"] ): bi = self.get_build_info(key) if bi is not None: description["firmware"][idn_key] = bi.to_idn_dict() description["firmware"]["driver"] = get_build_info().to_idn_dict() return description
# ------------------------------------------------------------------------
[docs] def to_tuple(self) -> tuple: """ Formats this device information object as a tuple for ordering purposes. Parameters ---------- Returns ------- tuple A tuple, containing all the information in this structure in a canonical format. """ return ( self._manufacturer, self._model, self._serial, self._sw_build.to_tuple() if self._sw_build is not None else None, self._fw_build.to_tuple() if self._fw_build is not None else None, self._kmod_build.to_tuple() if self._kmod_build is not None else None, self._cfg_man_build.to_tuple() if self._cfg_man_build is not None else None, )
# ------------------------------------------------------------------------ def __eq__(self, other) -> bool: if isinstance(other, DeviceInfo): return self.to_tuple() == other.to_tuple() return NotImplemented # ------------------------------------------------------------------------ def __ne__(self, other) -> bool: if isinstance(other, DeviceInfo): return self.to_tuple() != other.to_tuple() return NotImplemented # ------------------------------------------------------------------------ def __str__(self) -> str: return f"{self.manufacturer} {self.model}"
# -- functions -----------------------------------------------------------------
[docs] def get_build_info() -> BuildInfo: """ Get build information for Qblox Instruments. Parameters ---------- Returns ------- BuildInfo Build information structure for Qblox Instruments. """ return BuildInfo( version="0.14.2", build="08/11/2024-15:53:06", hash="0x9b9bb3c0", dirty=False )
# Set version. __version__ = get_build_info().version_str