API Documentation
A single page with the entire API reference.
tufup.client:
- class tufup.client.Client(app_name: str, app_install_dir: Path, current_version: str, metadata_dir: Path, metadata_base_url: str, target_dir: Path, target_base_url: str, extract_dir: Path | None = None, refresh_required: bool = False, session_auth: Dict[str, Tuple[str, str] | AuthBase] | None = None, binary_diff: type[BinaryDiff] | None = None, **kwargs)
- __init__(app_name: str, app_install_dir: Path, current_version: str, metadata_dir: Path, metadata_base_url: str, target_dir: Path, target_base_url: str, extract_dir: Path | None = None, refresh_required: bool = False, session_auth: Dict[str, Tuple[str, str] | AuthBase] | None = None, binary_diff: type[BinaryDiff] | None = None, **kwargs)
The tufup.client.Client is a subclass of tuf.ngclient.Updater.
for session_auth formats, see docs for tufup.client.AuthRequestsFetcher
- property trusted_target_metas: list
Return a list of available trusted targets, as TargetMeta objects.
This is convenient because TargetMeta objects can be sorted by version.
- get_targetinfo(target_path: str | TargetMeta) TargetFile | None
Extend Updater.get_targetinfo to handle TargetMeta input args.
- property updates_available
- download_and_apply_update(skip_confirmation: bool = False, install: Callable | None = None, progress_hook: Callable | None = None, **kwargs)
Download and apply available updates.
Note that check_for_updates must be called first.
This downloads the files found by check_for_updates, applies any patches, and extracts the resulting archive to the extract_dir. At that point, the update is ready to be installed (i.e. moved into place). This is done by calling install with the specified **kwargs.
The default install callable moves the content of extract_dir to app_install_dir, and exits the application (not necessarily in that order).
The **kwargs are passed on to the ‘install’ callable
The default install callable accepts two additional arguments:
purge_dst_dir (default False): if True, ALL content will be deleted from the app_install_dir
exclude_from_purge (default None): list of paths to exclude from purge
DANGER: Only set purge_dst_dir=True if your app is installed in its own separate directory, otherwise this will cause unrelated files and folders to be deleted.
- check_for_updates(pre: str | None = None, patch: bool = True, ignore_required: bool = False) TargetMeta | None
Check if any updates are available, based on current app version.
Returns latest archive meta, if a new archive is found.
Final releases are always included. Pre-releases are excluded by default. If pre is specified, pre-releases are included, down to the specified level. Pre-release identifiers follow the PEP440 specification, i.e. ‘a’, ‘b’, or ‘rc’, for alpha, beta, and release candidate, respectively.
If patch is False, a full update is enforced.
If a new release is marked as “required” (in its custom metadata) this release will take precedence over any non-required releases, even if the latter are newer. This may be useful e.g. in case of a configuration change. These “required” releases should be rare, and should preferably be avoided. However, in the exceedingly rare event that there are “required” updates, yet the user wants to treat them as non-required, they can specify ignore_required=True.
- download_target(targetinfo: TargetFile, filepath: str | None = None, target_base_url: str | None = None) str
Download the target file specified by
targetinfo.- Args:
targetinfo:
TargetFilefromget_targetinfo(). filepath: Local path to download into. IfNone, the file isdownloaded into directory defined by
target_dirconstructor argument using a generated filename. If file already exists, it is overwritten.- target_base_url: Base URL used to form the final target
download URL. Default is the value provided in
Updater()
- Raises:
ValueError: Invalid arguments DownloadError: Download of the target file failed in some way RepositoryError: Downloaded target failed to be verified in some way OSError: Failed to write target to file
- Returns:
Local path to downloaded file
- find_cached_target(targetinfo: TargetFile, filepath: str | None = None) str | None
Check whether a local file is an up to date target.
- Args:
targetinfo:
TargetFilefromget_targetinfo(). filepath: Local path to file. IfNone, a file path isgenerated based on
target_dirconstructor argument.- Raises:
ValueError: Incorrect arguments
- Returns:
Local file path if the file is an up to date target file.
Noneif file is not found or it is not up to date.
- refresh() None
Refresh top-level metadata.
Downloads, verifies, and loads metadata for the top-level roles in the specified order (root -> timestamp -> snapshot -> targets) implementing all the checks required in the TUF client workflow.
A
refresh()can be done only once during the lifetime of an Updater. Ifrefresh()has not been explicitly called before the firstget_targetinfo()call, it will be done implicitly at that time.The metadata for delegated roles is not updated by
refresh(): that happens on demand duringget_targetinfo(). However, if the repository uses consistent_snapshot, then all metadata downloaded by the Updater will use the same consistent repository state.- Raises:
OSError: New metadata could not be written to disk RepositoryError: Metadata failed to verify in some way DownloadError: Download of a metadata file failed in some way
- class tufup.client.AuthRequestsFetcher(session_auth: Dict[str, Tuple[str, str] | AuthBase] | None = None)
- __init__(session_auth: Dict[str, Tuple[str, str] | AuthBase] | None = None) None
This extends the default tuf RequestsFetcher, so we can specify authentication tuples (or custom authentication objects) for each session.
session_auth (optional):
dict of the form {<scheme and server>: (<username>, <password>), …} or {<scheme and server>: <requests.auth.AuthBase>, …} or some combination of those
where <scheme and server> can be e.g. https://example.com or http://localhost:8000.
Note: <scheme and server> must not have a trailing slash
Naming follows [RFC 2396][1], which defines a generic uri as:
<scheme>://<authority><path>?<query>
where <authority> can be <server>.
Also see session authentication example in requests docs: [1][2][3]
[1]: https://datatracker.ietf.org/doc/html/rfc2396#section-3 [2]: https://docs.python-requests.org/en/master/user/advanced/#session-objects [3]: https://docs.python-requests.org/en/latest/user/advanced/#custom-authentication [4]: https://docs.python-requests.org/en/master/api/#sessionapi
- attach_progress_hook(hook: Callable, bytes_expected: int)
Allow clients to attach a progress hook which gets called after every downloaded chunk.
The hook must accept two kwargs: bytes_downloaded and bytes_expected
- download_bytes(url: str, max_length: int) bytes
Download bytes from given
url.Returns the downloaded bytes, otherwise like
download_file().- Args:
url: URL string that represents the location of the file. max_length: Upper bound of data size in bytes.
- Raises:
exceptions.DownloadError: An error occurred during download. exceptions.DownloadLengthMismatchError: Downloaded bytes exceed
max_length.exceptions.DownloadHTTPError: An HTTP error code was received.
- Returns:
Content of the file in bytes.
- download_file(url: str, max_length: int) Iterator[IO]
Download file from given
url.It is recommended to use
download_file()within awithblock to guarantee that allocated file resources will always be released even if download fails.- Args:
url: URL string that represents the location of the file. max_length: Upper bound of file size in bytes.
- Raises:
exceptions.DownloadError: An error occurred during download. exceptions.DownloadLengthMismatchError: Downloaded bytes exceed
max_length.exceptions.DownloadHTTPError: An HTTP error code was received.
- Yields:
TemporaryFileobject that points to the contents ofurl.
- fetch(url: str) Iterator[bytes]
Fetch the contents of HTTP/HTTPS
urlfrom a remote server.- Args:
url: URL string that represents a file location.
- Raises:
exceptions.DownloadError: An error occurred during download. exceptions.DownloadHTTPError: An HTTP error code was received.
- Returns:
Bytes iterator
tufup.repo:
- tufup.repo.in_(days: float) datetime
Returns a timestamp for the specified number of days from now.
- class tufup.repo.Keys(dir_path: Path | str | None = None, encrypted: List[str] | None = None, key_map: RolesDict | None = None, thresholds: RolesDict | None = None)
- filename_pattern = '{key_name}'
- __init__(dir_path: Path | str | None = None, encrypted: List[str] | None = None, key_map: RolesDict | None = None, thresholds: RolesDict | None = None)
dir_path: directory where all key files are stored encrypted: names of the keys that are (to be) encrypted key_map: maps top-level role names to lists of key names
- import_all_public_keys()
- import_public_key(role_name: str, key_name: str | None = None)
Import public key for specified role.
- create()
- static create_key_pair(private_key_path: Path, encrypted: bool) Path
- private_key_path(key_name: str) Path
- public_key_path(key_name: str) Path
- public()
- roles()
- classmethod find_private_key(key_name: str, key_dirs: List[Path | str]) Path | None
recursively search key_dirs for a private key with specified key_name
returns path to first matching file (or None)
- tufup.repo.make_gztar_archive(src_dir: Path | str, dst_dir: Path | str, app_name: str, version: str, tar_format: int = 2) TargetMeta | None
Create a gzipped tar archive in the dst_dir, based on content of src_dir.
The PAX_FORMAT is currently the default tar format [1] used by the tarfile module. For improved portability [2] and reproducibility [3], this can be changed e.g. to USTAR_FORMAT.
[1]: https://www.gnu.org/software/tar/manual/html_node/Formats.html#Formats [2]: https://www.gnu.org/software/tar/manual/html_node/Portability.html#Portability [3]: https://www.gnu.org/software/tar/manual/html_node/Reproducibility.html#Reproducibility
- class tufup.repo.Repository(app_name: str, app_version_attr: str | None = None, repo_dir: Path | str | None = None, keys_dir: Path | str | None = None, key_map: RolesDict | None = None, encrypted_keys: List[str] | None = None, expiration_days: RolesDict | None = None, thresholds: RolesDict | None = None, binary_diff: type[BinaryDiff] | None = None)
High-level tools for repository management.
- config_filename = '.tufup-repo-config'
- __init__(app_name: str, app_version_attr: str | None = None, repo_dir: Path | str | None = None, keys_dir: Path | str | None = None, key_map: RolesDict | None = None, encrypted_keys: List[str] | None = None, expiration_days: RolesDict | None = None, thresholds: RolesDict | None = None, binary_diff: type[BinaryDiff] | None = None)
- property config_dict
dict to be saved to configuration file.
- property metadata_dir: Path
- property targets_dir: Path
- property app_version: str
- classmethod get_config_file_path() Path
- save_config()
Save current configuration.
- classmethod load_config() dict
Load configuration dict from file.
- classmethod from_config()
Create Repository instance from configuration file.
- initialize(extra_key_dirs: List[Path] | None = None)
Initialize (or update) the local repository.
This includes:
create directories if they do not exist
import public keys from existing files, or create new key pairs
import roles from existing metadata files
create root metadata file if it does not exist
Safe to call for existing keys and roles.
- refresh_expiration_date(role_name: str, days: int | None = None)
- replace_key(old_key_name: str, new_public_key_path: Path | str, new_private_key_encrypted: bool)
Replace an existing key by a new one, e.g. after a key compromise.
Note the changes are not published yet: call publish_changes() for that.
- add_key(role_name: str, public_key_path: Path | str, encrypted: bool)
Register a new public key for the specified role.
Note the changes are not published yet: call publish_changes() for that.
- add_bundle(new_bundle_dir: Path | str, new_version: str | None = None, skip_patch: bool = False, custom_metadata: dict | None = None, required: bool = False)
Adds a new application bundle to the local repository.
An archive file is created from the app bundle, and this file is added to the tuf repository. If a previous archive version is found, a patch file is also created and added to the repository, unless skip_patch is True.
Optional custom_metadata can be specified as a dictionary.
If required=True (default is False), this release will always be installed, even if newer releases are available. For example, suppose an app is running at version 1.0, and version 2.0 is required, but version 3.0 is also available, then tufup will first update to version 2.0, before updating to 3.0 on the next run.
Note the changes are not published yet: call publish_changes() for that.
- remove_latest_bundle()
Removes the latest app bundle from the local repository.
This deletes the bundle’s archive file and corresponding patch file from the targets directory, and updates the tuf repository metadata accordingly.
Note the changes are not published yet: call publish_changes() for that
- publish_changes(private_key_dirs: List[Path | str])
Publish all modified roles. That is, if a role has changed w.r.t. to the version on disk:
update expiration date (if not yet updated)
bump version (if not yet bumped)
sign
save to disk
If a role has not been modified, it is skipped.
- threshold_sign(role_name: str, private_key_dirs: List[Path | str]) int
Sign the metadata file for a specific role, and save changes to disk.
Use this to sign and save without making any changes to the actual signed metadata.
In case of root key rotation, both the old private key and the new private key are required.
Returns the number of signatures created.
- class tufup.repo.Roles(dir_path: Path | str | None = None)
- filename_pattern = '{version}{role_name}{suffix}'
- __init__(dir_path: Path | str | None = None)
TUF roles
- root metadata tells us:
all the known keys (key id and public key value)
which keys belong to each role
how many signatures are needed for each role
- targets metadata tells us:
which target files are available (filename, size, hash)
- snapshots metatadata tells us:
which version of the targets-metadata file to expect
- timestamp metadata tells us:
which version of the snapshot-metadata file to expect
- add_or_update_target(local_path: Path | str, url_path_segments: List[str] | None = None, custom: CustomMetadataDict | None = None)
- remove_target(local_path: Path | str) bool
- add_public_key(role_name: str, public_key_path: Path | str)
Import a public key from file and add it to the specified role.
- set_signature_threshold(role_name: str, threshold: int)
- set_expiration_date(role_name: str, days: int)
- sign_role(role_name: str, private_key_path: Path | str)
Sign role using specified private key.
We sign off on the role.signed part, and the signature is added to the role.signatures list.
- file_path(role_name: str, version: int | None = None)
- file_exists(role_name: str)
Return True if any metadata file exists for the specified role, ignoring any versions in the filename.
- persist_role(role_name: str)
Save specified role to corresponding metadata file.
In case of root, make sure “root.json” always represents the latest version (in addition to x.root.json).
- get_latest_archive() TargetMeta | None
Returns TargetMeta for latest archive.
Note that all pre-release versions are always included: On the repo side, there is no difference between final releases an pre-releases. Pre-release specifiers are only used on the Client side, to filter available updates).
tufup.common:
- class tufup.common.CustomMetadataDict
explicitly separate custom metadata into user-specified metadata and metadata used by tufup internally
- user: dict
- tufup: dict
- __init__(*args, **kwargs)
- clear() None. Remove all items from D.
- copy() a shallow copy of D
- fromkeys(value=None, /)
Create a new dictionary with keys from iterable and values set to value.
- get(key, default=None, /)
Return the value for key if key is in the dictionary, else default.
- items() a set-like object providing a view on D's items
- keys() a set-like object providing a view on D's keys
- pop(k[, d]) v, remove specified key and return the corresponding value.
If key is not found, default is returned if given, otherwise KeyError is raised
- popitem()
Remove and return a (key, value) pair as a 2-tuple.
Pairs are returned in LIFO (last-in, first-out) order. Raises KeyError if the dict is empty.
- setdefault(key, default=None, /)
Insert key with a value of default if key is not in the dictionary.
Return the value for key if key is in the dictionary, else default.
- update([E, ]**F) None. Update D from dict/iterable E and F.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]
- values() an object providing a view on D's values
- class tufup.common.TargetMeta(target_path: Path | str | None = None, name: str | None = None, version: str | None = None, is_archive: bool | None = True, custom: CustomMetadataDict | None = None)
- filename_pattern = '{name}-{version}{suffix}'
- filename_regex = re.compile('^(?P<name>[\\w-]+)-(?P<version>.+)(?P<suffix>\\.tar\\.gz|\\.patch)$')
- __init__(target_path: Path | str | None = None, name: str | None = None, version: str | None = None, is_archive: bool | None = True, custom: CustomMetadataDict | None = None)
Initialize either with target_path, or with name, version, archive.
BEWARE: whitespace is not allowed in the filename, nor in the name or version arguments
- property custom: dict
returns user-specified custom metadata dict
- property custom_internal: dict
returns tufup-internal custom metadata dict
- property filename
- property name: str | None
The app name.
- property version: Version | None
- property suffix: str | None
Returns the filename suffix, either ‘.tar.gz’, ‘.patch’, or None.
- property is_archive: bool
- property is_patch: bool
- property is_other: bool
- classmethod parse_filename(filename: str) dict
Parse a filename to extract app name, version, and suffix.
We do not impose any versioning requirements yet, such as defined in packaging.version.VERSION_PATTERN.
- classmethod compose_filename(name: str, version: str, is_archive: bool)
- class tufup.common.BinaryDiff
BinaryDiff represents an interface for overriding the binary diff/patch functions
- abstract static diff(*, src_bytes: bytes, dst_bytes: bytes) bytes
Create patch as the binary difference between source and destination data
- abstract static patch(*, src_bytes: bytes, patch_bytes: bytes) bytes
Apply binary patch to source data to recover destination data
- class tufup.common.DefaultBinaryDiff
The default implementation of binary differencing and patching functions
- diff(src_bytes, dst_bytes) bytes
Return a BSDIFF4-format patch (from src_bytes to dst_bytes) as bytes.
- patch(src_bytes, patch_bytes) bytes
Apply the BSDIFF4-format patch_bytes to src_bytes and return the bytes.
- class tufup.common.Patcher
- DEFAULT_HASH_ALGORITHM = 'sha256'
- classmethod diff_and_hash(src_path: Path, dst_path: Path, patch_path: Path, binary_diff: type[BinaryDiff] | None = None) dict
Creates a patch file from the binary difference between source and destination .tar archives. The source and destination files are expected to be gzip-compressed tar archives (.tar.gz).
The binary differencing method can be customized by implementing a BinaryDiff subclass and passing this in via the binary_diff argument.
Returns a dict with size and hash of the uncompressed destination archive.
- classmethod patch_and_verify(src_path: Path, dst_path: Path, patch_targets: Dict[TargetMeta, Path], binary_diff: type[BinaryDiff] | None = None) None
Applies one or more binary patch files to a source file in order to reconstruct a destination file.
Source file and destination file are gzip-compressed tar archives, but the patches are applied to the uncompressed tar archives. The reason is that small changes in uncompressed data can cause (very) large differences in gzip compressed data, leading to excessively large patch files (see #69).
The integrity of the patched .tar archive is verified using expected length and hash (from custom tuf metadata), similar to python-tuf’s download verification. If the patched archive fails this check, the destination file is not written.
The binary patching method can be customized by implementing a BinaryDiff subclass and passing this in via the binary_diff argument.