Skip to content

Services layer

Business logic services and orchestration for EzCompiler.

The services layer implements the core business logic, handling compilation, configuration, template processing, and upload operations.


CompilerService

Service for managing compiler selection and orchestrating the compilation process.

CompilerService

CompilerService(config: CompilerConfig)

Compilation orchestration service.

Orchestrates project compilation using different compiler backends. Handles compiler selection, validation, and execution.

Attributes:

Name Type Description
_config

CompilerConfig instance with project settings

Example

config = CompilerConfig(...) service = CompilerService(config) result = service.compile(console=True, compiler="PyInstaller") print(result.zip_needed) False

Initialize the compiler service.

Parameters:

Name Type Description Default
config CompilerConfig

CompilerConfig instance with project settings

required

Raises:

Type Description
ConfigurationError

If config is None or invalid

compiler_instance property

compiler_instance: CompilerPort | None

Get the current compiler instance.

Returns:

Type Description
CompilerPort | None

CompilerPort | None: Current compiler instance or None if not compiled yet

compile

compile(
    console: bool = True,
    compiler: _CompilerName | None = None,
) -> CompilationResult

Compile the project using specified or auto-selected compiler.

Validates configuration, selects compiler if not specified, and executes compilation. Returns result with zip_needed flag.

Parameters:

Name Type Description Default
console bool

Whether to show console window (default: True)

True
compiler _CompilerName | None

Compiler to use or None for auto-selection - "Cx_Freeze": Creates directory with dependencies - "PyInstaller": Creates single executable - "Nuitka": Creates standalone folder or single executable - None: Use config compiler, or prompt user if none set

None

Returns:

Name Type Description
CompilationResult CompilationResult

Result with zip_needed flag and compiler instance

Raises:

Type Description
ConfigurationError

If project not initialized

CompilationError

If compilation fails

Example

service = CompilerService(config) result = service.compile(console=False, compiler="PyInstaller") if result.zip_needed: ... # Create ZIP archive

use_existing_build

use_existing_build(
    compiler: _CompilerName | None = None,
) -> CompilationResult

Reuse a previous build from output_folder instead of compiling.

Resolves the compiler (without running it) so downstream stages (zip, installer, release) can resume from an already compiled output folder.

Parameters:

Name Type Description Default
compiler _CompilerName | None

Compiler used for the previous build, or None to use config.compiler (or prompt when unset)

None

Returns:

Name Type Description
CompilationResult CompilationResult

Result describing the existing build

Raises:

Type Description
CompilationError

If output_folder is missing or empty, or the compiler name is invalid

Example

service = CompilerService(config) result = service.use_existing_build(compiler="Cx_Freeze")


ConfigService

Service for loading, validating, and managing configuration.

ConfigService

Configuration orchestration service.

Orchestrates configuration loading from multiple sources with a cascade merge system. Sources are merged in priority order (later wins):

  1. pyproject.toml [project] + [tool.ezcompiler] (base)
  2. ezcompiler.yaml or ezcompiler.json (override)
  3. CLI arguments (final override)
Example

config = ConfigService.build_compiler_config() config = ConfigService.build_compiler_config( ... config_path=Path("ezcompiler.yaml"), ... cli_overrides={"compiler": "PyInstaller"}, ... )

load_config staticmethod

load_config(
    config_path: Path | None = None,
    pyproject_path: Path | None = None,
    cli_overrides: dict[str, Any] | None = None,
    search_dir: Path | None = None,
) -> dict[str, Any]

Load configuration with full cascade.

Merges configuration from multiple sources in priority order: 1. pyproject.toml [project] + [tool.ezcompiler] (base layer) 2. ezcompiler.yaml or .json - explicit or auto-discovered (override) 3. CLI overrides (final override)

Parameters:

Name Type Description Default
config_path Path | None

Explicit config file path for YAML/JSON (skips auto-discovery)

None
pyproject_path Path | None

Explicit pyproject.toml path (default: search_dir/pyproject.toml)

None
cli_overrides dict[str, Any] | None

Dict of CLI-provided overrides (only non-default values)

None
search_dir Path | None

Directory to search for config files (default: cwd)

None

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Merged configuration dictionary

Raises:

Type Description
ConfigurationError

If no configuration source found or loading fails

build_compiler_config staticmethod

build_compiler_config(
    config_path: Path | None = None,
    pyproject_path: Path | None = None,
    cli_overrides: dict[str, Any] | None = None,
    search_dir: Path | None = None,
) -> CompilerConfig

Load configuration and create a CompilerConfig instance.

Convenience method that combines load_config() with CompilerConfig.from_dict().

Parameters:

Name Type Description Default
config_path Path | None

Explicit config file path for YAML/JSON

None
pyproject_path Path | None

Explicit pyproject.toml path

None
cli_overrides dict[str, Any] | None

Dict of CLI-provided overrides

None
search_dir Path | None

Directory to search for config files (default: cwd)

None

Returns:

Name Type Description
CompilerConfig CompilerConfig

Validated configuration instance

Raises:

Type Description
ConfigurationError

If configuration is invalid or not found

load_pyproject_as_dict staticmethod

load_pyproject_as_dict(
    pyproject_path: Path,
) -> dict[str, Any]

Load and extract configuration from a pyproject.toml file.

Parameters:

Name Type Description Default
pyproject_path Path

Path to the pyproject.toml file

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Extracted configuration dictionary

Raises:

Type Description
ConfigurationError

If the file cannot be loaded or parsed

merge_configs staticmethod

merge_configs(
    base: dict[str, Any], override: dict[str, Any]
) -> dict[str, Any]

Deep-merge two configuration dictionaries (override wins on conflicts).

Parameters:

Name Type Description Default
base dict[str, Any]

Base configuration dictionary

required
override dict[str, Any]

Override dictionary (values win over base)

required

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Merged configuration dictionary


TemplateService

Service for processing templates and generating dynamic files.

TemplateService

TemplateService(file_writer: BaseFileWriter | None = None)

Template processing and file generation service.

Orchestrates template loading, processing, and file generation for configuration files, build.py, and version information files.

Attributes:

Name Type Description
_template_loader

TemplateLoader instance for template operations

_processor

TemplateProcessor instance for variable substitution

Example

service = TemplateService() config = {"version": "1.0.0", "project_name": "MyApp"} service.generate_setup_file(config, Path("build.py")) service.generate_version_file(config, Path("version_info.txt"))

Initialize the template service.

Creates TemplateLoader and TemplateProcessor instances for template operations.

generate_config_file

generate_config_file(
    config: dict[str, Any],
    output_path: Path,
    format_type: str = "yaml",
) -> None

Generate a configuration file (YAML or JSON) from template.

Parameters:

Name Type Description Default
config dict[str, Any]

Project configuration dictionary

required
output_path Path

Path where to save the config file

required
format_type str

Format type ("yaml" or "json")

'yaml'

Raises:

Type Description
TemplateError

If generation fails

Example

config = {"version": "1.0.0", "project_name": "MyApp"} service.generate_config_file(config, Path("ezcompiler.yaml"), "yaml")

generate_setup_file

generate_setup_file(
    config: dict[str, Any],
    output_path: Path | None = None,
    output_dir: Path | None = None,
) -> Path

Generate a build.py file from template.

Parameters:

Name Type Description Default
config dict[str, Any]

Project configuration dictionary

required
output_path Path | None

Direct path to build.py file (optional)

None
output_dir Path | None

Directory where to save build.py (optional, defaults to current dir)

None

Returns:

Name Type Description
Path Path

Path to the generated build.py file

Raises:

Type Description
TemplateError

If generation fails

Example

config = {"version": "1.0.0", "project_name": "MyApp"} setup_path = service.generate_setup_file(config, output_dir=Path("build"))

generate_version_file

generate_version_file(
    config: dict[str, Any],
    output_path: Path | None = None,
    format_type: str = "txt",
) -> Path

Generate a version information file from template.

Parameters:

Name Type Description Default
config dict[str, Any]

Project configuration dictionary

required
output_path Path | None

Path where to save the version file (optional)

None
format_type str

Template format type (default: "txt")

'txt'

Returns:

Name Type Description
Path Path

Path to the generated version file

Raises:

Type Description
VersionError

If generation fails

Note

If output_path is None, uses version_filename and output_folder from config.

Example

config = { ... "version": "1.0.0", ... "project_name": "MyApp", ... "version_filename": "version_info.txt", ... "output_folder": "dist" ... } version_path = service.generate_version_file(config)

process_config_template

process_config_template(
    format_type: str, config: dict[str, Any]
) -> str

Process a configuration template with values.

Parameters:

Name Type Description Default
format_type str

Format type ("yaml" or "json")

required
config dict[str, Any]

Configuration dictionary

required

Returns:

Name Type Description
str str

Processed template content

Example

config = {"version": "1.0.0", "project_name": "MyApp"} content = service.process_config_template("yaml", config)

process_setup_template

process_setup_template(config: dict[str, Any]) -> str

Process a setup template with values.

Parameters:

Name Type Description Default
config dict[str, Any]

Configuration dictionary

required

Returns:

Name Type Description
str str

Processed template content

Example

config = {"version": "1.0.0", "project_name": "MyApp"} content = service.process_setup_template(config)

process_version_template

process_version_template(
    version: str,
    company_name: str,
    project_description: str,
    project_name: str,
    format_type: str = "txt",
) -> str

Process a version template with values.

Parameters:

Name Type Description Default
version str

Project version

required
company_name str

Company name

required
project_description str

Project description

required
project_name str

Project name

required
format_type str

Format type (default: "txt")

'txt'

Returns:

Name Type Description
str str

Processed template content

Example

content = service.process_version_template( ... "1.0.0", "MyCompany", "Description", "MyApp" ... )

list_available_templates

list_available_templates() -> dict[str, list[str]]

List all available templates.

Returns:

Type Description
dict[str, list[str]]

dict[str, list[str]]: Dictionary mapping template types to available formats

Example

templates = service.list_available_templates() print(templates)

validate_template

validate_template(
    template_type: str, format_type: str
) -> bool

Validate a template file.

Parameters:

Name Type Description Default
template_type str

Type of template (config, version, setup)

required
format_type str

Format of the template

required

Returns:

Name Type Description
bool bool

True if template is valid, False otherwise

Example

is_valid = service.validate_template("config", "yaml")

generate_mockup_template

generate_mockup_template(
    template_type: str, format_type: str, output_path: Path
) -> None

Generate a template file with mockup values instead of placeholders.

Parameters:

Name Type Description Default
template_type str

Type of template (config, version, setup)

required
format_type str

Format of the template (yaml, json, py, txt)

required
output_path Path

Path where to save the generated file

required

Raises:

Type Description
TemplateError

If generation fails

Example

service.generate_mockup_template("config", "yaml", Path("ezcompiler.yaml"))

generate_raw_template

generate_raw_template(
    template_type: str, format_type: str, output_path: Path
) -> None

Generate a raw template file with placeholders.

Parameters:

Name Type Description Default
template_type str

Type of template (config, version, setup)

required
format_type str

Format of the template (yaml, json, py, txt)

required
output_path Path

Path where to save the template file

required

Raises:

Type Description
TemplateError

If generation fails

Example

service.generate_raw_template("config", "yaml", Path("template.yaml"))


PipelineService

Service for orchestrating the full compile → zip → upload pipeline with injectable factory.

PipelineService

PipelineService(
    compiler_service_factory: Callable[
        [CompilerConfig], CompilerService
    ]
    | None = None,
)

Service that coordinates compile, zip and upload stages.

Initialise the pipeline service.

Parameters:

Name Type Description Default
compiler_service_factory Callable[[CompilerConfig], CompilerService] | None

Optional factory to create a CompilerService from a CompilerConfig. Defaults to CompilerService constructor. Inject a custom factory in tests to avoid triggering real compilation.

None

compile_project

compile_project(
    config: CompilerConfig,
    console: bool = True,
    compiler: str | None = None,
) -> tuple[CompilerService, CompilationResult]

Compile a project and return service + result.

reuse_build

reuse_build(
    config: CompilerConfig, compiler: str | None = None
) -> tuple[CompilerService, CompilationResult]

Reuse an existing build (skip compilation) and return service + result.

zip_artifact

zip_artifact(
    config: CompilerConfig,
    compiler_service: CompilerService,
    compilation_result: CompilationResult | None,
    progress_callback: Callable[[str, int], None]
    | None = None,
) -> bool

Create ZIP artifact when required and return True when created.

build_stages staticmethod

build_stages(
    config: CompilerConfig,
    should_zip: bool = False,
    should_upload: bool = False,
    should_release: bool = False,
    should_installer: bool = False,
    should_build: bool = True,
) -> list[dict[str, Any]]

Build the stage list for dynamic_layered_progress.

Parameters:

Name Type Description Default
config CompilerConfig

Compiler configuration (used for display labels)

required
should_zip bool

Whether a ZIP stage should be included

False
should_upload bool

Whether an upload stage should be included

False
should_release bool

Whether a TUF release stage should be included

False
should_installer bool

Whether an installer stage should be included

False
should_build bool

Whether version + compile stages run; when False a single "reuse existing build" stage replaces them

True

Returns:

Type Description
list[dict[str, Any]]

list[dict]: Stage configuration list ready for dynamic_layered_progress

upload_artifact

upload_artifact(
    config: CompilerConfig,
    structure: str,
    destination: str,
    compilation_result: CompilationResult | None,
    upload_config: dict[str, Any] | None = None,
) -> None

Upload project artifact to a destination.

resolve_publishable_assets staticmethod

resolve_publishable_assets(
    config: CompilerConfig,
) -> tuple[Path | None, Path | None]

Resolve the publishable artifacts, with the same contract everywhere.

Both publication paths (platform and files) rely on this resolution: an enabled but missing installer fails whatever the destination, instead of being published silently to disk/server/r2.

Parameters:

Name Type Description Default
config CompilerConfig

Configuration (supplies installer, zip_file_path, paths).

required

Returns:

Name Type Description
tuple tuple[Path | None, Path | None]

(installer, zip), each element being None when it is not expected (installer disabled) or absent (single-file compilation without a zip).

Raises:

Type Description
ReleaseError

If the enabled installer is missing, or if no publishable artifact exists.

assemble_release_dir staticmethod

assemble_release_dir(config: CompilerConfig) -> Path

Assemble the release directory holding the zip and the installer.

Layout (cleaned on every run)::

release/
├── <App>.zip                     (if the file exists)
└── <App>-<version>-setup.exe      (if installer.enabled=True)

The TUF tree (metadata/ + targets/) stays in tufup_repo_dir and is pushed straight to the update backend by upload().

Artifact resolution is shared by both publication paths (see resolve_publishable_assets): an enabled but missing installer fails here too.

Parameters:

Name Type Description Default
config CompilerConfig

Configuration (supplies output_folder and zip_file_path).

required

Returns:

Name Type Description
Path Path

The assembled release/ directory.

stage_versioned_assets staticmethod

stage_versioned_assets(
    config: CompilerConfig,
) -> list[Path]

Resolve the publishable artifacts and version the zip name.

config.zip_file_path is dist/<Project>.zip — without a version. Publishing that name as is would produce assets indistinguishable between two releases, so the zip is copied to <Project>-<version>.zip. The copy is idempotent: running the method again changes nothing.

A project without an installer (installer.enabled = False) is a legitimate case: the list then holds the zip alone. An enabled but missing installer, however, is an error. The zip stays optional: a single-file compilation does not produce one.

Versioned copies accumulate in dist/ (one per version); they are never cleaned up automatically.

Parameters:

Name Type Description Default
config CompilerConfig

Configuration (supplies project_name, version, paths).

required

Returns:

Type Description
list[Path]

list[Path]: The existing assets — installer first, then zip.

Raises:

Type Description
ReleaseError

If the enabled installer is missing, or if no publishable artifact exists.

release_artifact staticmethod

release_artifact(
    config: CompilerConfig,
    compilation_result: CompilationResult | None,
    *,
    required: bool = False,
) -> Path

Build the local TUF repo from output_folder. Never publishes.

build_installer staticmethod

build_installer(
    config: CompilerConfig,
    compilation_result: CompilationResult | None,
) -> Path | None

Build the Inno Setup installer when installer.enabled=True.


UploaderService

Service for orchestrating upload operations to various backends.

UploaderService

Upload orchestration service.

Orchestrates file and directory uploads using different upload backends. Handles upload type selection, validation, and execution.

Example

service = UploaderService() service.upload( ... source_path=Path("dist.zip"), ... upload_type="disk", ... destination="releases/", ... upload_config={"overwrite": True} ... )

upload staticmethod

upload(
    source_path: Path,
    upload_type: UploadType,
    destination: str,
    upload_config: dict[str, Any] | None = None,
) -> None

Upload a file or directory to the specified destination.

Parameters:

Name Type Description Default
source_path Path

Path to the source file or directory

required
upload_type UploadType

Type of upload ("disk" or "server")

required
destination str

Destination path or URL

required
upload_config dict[str, Any] | None

Additional uploader configuration options

None

Raises:

Type Description
UploadError

If upload fails or upload type is invalid

Example

UploaderService.upload( ... Path("dist.zip"), ... "disk", ... "releases/", ... {"overwrite": True} ... )

download staticmethod

download(
    remote_source: str,
    upload_type: UploadType,
    destination_local: Path,
    upload_config: dict[str, Any] | None = None,
) -> None

Download a remote tree into destination_local via the backend.

Parameters:

Name Type Description Default
remote_source str

Remote source (path, URL or prefix) to fetch.

required
upload_type UploadType

Backend type ("disk", "server" or "r2").

required
destination_local Path

Local directory to populate.

required
upload_config dict[str, Any] | None

Additional uploader configuration options.

None

Raises:

Type Description
UploadError

If the download fails or the type is invalid.

upload_release staticmethod

upload_release(
    config: CompilerConfig,
    repo_dir: Path,
    release_root: Path | None,
    destination: str | None = None,
    repo_destination: str | None = None,
    release_destination: str | None = None,
    upload_config: dict[str, Any] | None = None,
) -> None

Sequential double upload: TUF tree then installer zip.

Step 1 — upload the TUF tree to <dest>/update/ (or the R2 prefix). Step 2 — upload the installer zip to <dest>/release/ (skipped on R2).

Note: The double upload is not atomic. If the zip upload fails, the TUF repo is already online. On failure, run upload() again to resume.

Parameters:

Name Type Description Default
config CompilerConfig

CompilerConfig holding the destinations and R2 options.

required
repo_dir Path

Local directory of the TUF repo.

required
release_root Path | None

Local directory of the installer zip (None on R2).

required
destination str | None

Shared override for both destinations.

None
repo_destination str | None

Override for config.repo_destination.

None
release_destination str | None

Override for config.release_destination.

None
upload_config dict[str, Any] | None

Extra options passed to the uploaders.

None

Raises:

Type Description
UploadError

If an upload fails.

upload_tuf_repo staticmethod

upload_tuf_repo(
    config: CompilerConfig,
    repo_dir: Path,
    repo_dest: str,
    destination: str | None,
    upload_config: dict[str, Any] | None,
) -> None

Upload the public part of the TUF tree to the configured destination.

Only metadata/, targets/ and withdrawn.json are published, from a temporary copy (see staged_tuf_tree): the private keys stored under repo_dir are never transferred.

staged_tuf_tree staticmethod

staged_tuf_tree(
    repo_dir: Path, *, keys_dir: Path | None = None
) -> Iterator[Path]

Copy the public files of the TUF tree into a temporary directory.

The layout is preserved (metadata/, targets/, withdrawn.json at the root); the directory is removed on exit. The keystore is always excluded from the copy, even when it sits under metadata/ or targets/.

Parameters:

Name Type Description Default
repo_dir Path

Root of the local TUF tree.

required
keys_dir Path | None

Private keystore to exclude, on top of any directory named keystore.

None

Raises:

Type Description
UploadError

If repo_dir/metadata is missing (publishing an empty tree would overwrite the remote channel) or if the copy fails.

upload_release_zip staticmethod

upload_release_zip(
    config: CompilerConfig,
    release_root: Path,
    rel_dest: str,
    destination: str | None,
    upload_config: dict[str, Any] | None,
) -> None

Upload the installer zip to the configured destination.

get_supported_types staticmethod

get_supported_types() -> list[str]

Get list of supported upload types.

Returns:

Type Description
list[str]

list[str]: List of supported upload type names

Example

types = UploaderService.get_supported_types() print(types) ['disk', 'server']

validate_upload_config staticmethod

validate_upload_config(
    upload_type: UploadType,
    config: dict[str, Any] | None = None,
) -> bool

Validate configuration for a specific upload type.

Parameters:

Name Type Description Default
upload_type UploadType

Type of uploader to validate

required
config dict[str, Any] | None

Configuration to validate (default: None)

None

Returns:

Name Type Description
bool bool

True if configuration is valid, False otherwise

Example

is_valid = UploaderService.validate_upload_config( ... "disk", {"overwrite": True} ... )


UpdaterService

Service for generating client updater files (update.py, settings.py, root.json) for tufup-based auto-update workflows.

UpdaterService

Service that generates the tufup client update files.

generate staticmethod

generate(
    config: CompilerConfig, output_dir: Path
) -> list[Path]

Generate update.py, settings.py, and copy root.json.

Parameters:

Name Type Description Default
config CompilerConfig

Compiler configuration (must have tuf_enabled=True).

required
output_dir Path

Directory where files are written (created if absent).

required

Returns:

Type Description
list[Path]

List of generated file paths [settings.py, update.py, root.json].

Raises:

Type Description
UpdaterConfigError

If config is invalid (tuf_enabled=False, root.json absent, repo_public_url missing for non-disk).

UpdaterGenerationError

If writing files fails.


InstallerService

Service orchestrating first-deployment installer packaging via an installer adapter.

InstallerService

Service orchestrating first-deployment installer packaging.

build_installer staticmethod

build_installer(
    bundle_dir: Path,
    app_name: str,
    version: str,
    output_dir: Path,
    *,
    installer_type: str = "innosetup",
    installer_config: InstallerConfig | None = None,
    company_name: str = "",
    icon: str = "",
    main_file: str = "",
) -> Path

Build the installer executable for a compiled bundle.

Parameters:

Name Type Description Default
bundle_dir Path

Directory containing the compiled application.

required
app_name str

Application name.

required
version str

Application version string.

required
output_dir Path

Directory where the setup.exe is produced.

required
installer_type str

Installer backend to use (default: "innosetup").

'innosetup'
installer_config InstallerConfig | None

Config forwarded to the installer adapter.

None
company_name str

Publisher name; feeds the AppId GUID derivation (see _iss_renderer.resolve_app_id) — must match whatever generate iss uses for the same product, or the two builds mint different AppIds for one product.

''
icon str

Path to the setup wizard icon.

''
main_file str

Entry-point script; disambiguates the main executable when the bundle holds several and none matches app_name.

''

Returns:

Name Type Description
Path Path

The produced setup.exe path.

Raises:

Type Description
InstallerError

When installer packaging fails.

generate_iss_script staticmethod

generate_iss_script(
    config: CompilerConfig,
    output_path: Path,
    *,
    force: bool,
) -> Path

Render a standalone, committable .iss script for config.

Unlike the ephemeral build path, the icon is emitted verbatim (never absolutized): the standalone script is meant to be committed, and an absolute machine-specific path would defeat that. A relative icon triggers a warning, since ISCC resolves it against the .iss file's own directory rather than the process cwd.

Parameters:

Name Type Description Default
config CompilerConfig

Compiler configuration (provides installer, company_name, icon and project_name).

required
output_path Path

Destination .iss file path.

required
force bool

When False, refuse to overwrite an existing file.

required

Returns:

Name Type Description
Path Path

output_path, once written.

Raises:

Type Description
InstallerConfigError

If output_path already exists and force is False.


ReleaseService

Service orchestrating secure-release packaging (TUF) and, optionally, publication.

ReleaseService

Service orchestrating secure-release packaging and publication.

release_and_publish staticmethod

release_and_publish(
    bundle_dir: Path,
    app_name: str,
    version: str,
    repo_dir: Path,
    *,
    release_type: str = "tufup",
    publish: bool = False,
    pull_before: bool = False,
    upload_type: str | None = None,
    destination: str | None = None,
    releaser_config: dict[str, Any] | None = None,
    upload_config: dict[str, Any] | None = None,
    required: bool = False,
) -> Path

Build the local TUF repo, then optionally publish it.

Parameters:

Name Type Description Default
bundle_dir Path

Directory containing the compiled application.

required
app_name str

Application name (used by tufup to name bundles).

required
version str

Application version string.

required
repo_dir Path

Root directory for the local TUF repository tree.

required
release_type str

Release backend to use (default: "tufup").

'tufup'
publish bool

When True, transfer the repository/ tree via an uploader.

False
pull_before bool

When True, download the current remote tree into repo_dir before releasing (R2 source-of-truth cycle). Requires upload_type and destination.

False
upload_type str | None

Upload backend ("disk" or "server"). Required when publish=True.

None
destination str | None

Upload destination path or URL. Required when publish=True.

None
releaser_config dict[str, Any] | None

Extra config forwarded to the releaser adapter.

None
upload_config dict[str, Any] | None

Extra config forwarded to the uploader adapter.

None
required bool

Mark the version as mandatory for clients.

False

Returns:

Name Type Description
Path Path

The local repository/ tree path.

Raises:

Type Description
ValueError

When publish=True but upload_type or destination is missing.

ReleaseError

When the version is not above a withdrawn version, or when release packaging or publishing fails.

init_release staticmethod

init_release(
    app_name: str,
    repo_dir: Path,
    keys_dir: Path,
    *,
    release_type: str = "tufup",
    releaser_config: dict[str, Any] | None = None,
) -> bool

Create the releaser through the factory and delegate to init_keys.

Returns True if the init ran, False if it was already present (skip).

refresh_expiration staticmethod

refresh_expiration(
    app_name: str,
    repo_dir: Path,
    keys_dir: Path,
    *,
    roles: tuple[str, ...] = (
        "targets",
        "snapshot",
        "timestamp",
    ),
    days: int | None = None,
    release_type: str = "tufup",
    releaser_config: dict[str, Any] | None = None,
) -> Path

Re-sign the metadata to push back expiration without a new release.

Native tufup keep-alive for projects updated irregularly.

Returns the local TUF repo directory.


TufService

Service reading the local TUF tree without tufup (status, expirations), recording withdrawn versions in withdrawn.json, and enforcing the release version rule.

TufService

Local TUF tree operations shared by the CLI and the release path.

repo_dir staticmethod

repo_dir(config: CompilerConfig) -> Path

Local TUF repository directory.

keys_dir staticmethod

keys_dir(config: CompilerConfig) -> Path

Private signing keys directory.

withdrawn_versions staticmethod

withdrawn_versions(repo_dir: Path) -> list[str]

Versions removed from the tree, in withdrawal order.

Raises:

Type Description
ReleaseError

If withdrawn.json exists but is malformed — a silently ignored file would disable the version guard.

record_withdrawn staticmethod

record_withdrawn(
    repo_dir: Path,
    version: str,
    *,
    now: datetime | None = None,
) -> None

Append version to withdrawn.json.

The file is written atomically (temporary file in the same directory, then os.replace): an interrupted write never truncates it.

Raises:

Type Description
ReleaseError

If the existing file is malformed, or if the write fails — the message names the version to add by hand.

ensure_releasable staticmethod

ensure_releasable(repo_dir: Path, version: str) -> None

Refuse a version that is not above every withdrawn version.

Raises:

Type Description
ReleaseError

If version <= a withdrawn version (PEP 440), or equals a non-PEP 440 withdrawn string.

remove_latest staticmethod

remove_latest(
    config: CompilerConfig, *, release_type: str = "tufup"
) -> str

Remove the latest version from the local tree and record it.

The signed tree (metadata/, targets/ and withdrawn.json) is copied to a timestamped backup directory first, and the withdrawal is recorded before the irreversible mutation: a releaser failure (missing key, read-only keystore, expired role) can therefore never leave a half-mutated tree with the withdrawal unrecorded. On failure the three are restored from the backup, whose path is named in the error.

Returns:

Name Type Description
str str

The removed version.

Raises:

Type Description
ReleaseError

If withdrawn.json is malformed or cannot be written, if the tree holds no version, or if the backup cannot be taken (nothing is removed in any of those cases).

ReleaseError / SigningKeyError

From the releaser; the tree and the register are rolled back to their pre-removal state.

status staticmethod

status(config: CompilerConfig) -> TufStatus

Read the local tree: versions, flags, expirations, withdrawals.

Raises:

Type Description
ReleaseError

If the tree is not initialized or a metadata file cannot be read.

read_tree_version staticmethod

read_tree_version(
    config: CompilerConfig,
    *,
    allow_empty: Literal[False] = False,
) -> str
read_tree_version(
    config: CompilerConfig, *, allow_empty: bool
) -> str | None
read_tree_version(
    config: CompilerConfig, *, allow_empty: bool = False
) -> str | None

Return the highest version signed in the local TUF tree.

The recap of publish update names this version, not config.version: a config bumped without a rebuild would otherwise announce a version the tree does not carry.

Parameters:

Name Type Description Default
config CompilerConfig

Current configuration (repo dir and project name).

required
allow_empty bool

True after the only version was withdrawn: the empty tree is publishable and None is then returned.

False

Returns:

Type Description
str | None

str | None: Highest version among the <app>-<version> targets, or None if allow_empty and the tree holds no archive.

Raises:

Type Description
ReleaseError

If no signed targets.json exists or it names no archive of the project.

latest_tree_version staticmethod

latest_tree_version(
    repo_dir: Path, app_name: str
) -> str | None

Highest <app>-<version> archive signed in repo_dir.

Returns:

Type Description
str | None

str | None: The raw version spelling tufup used, or None when no signed targets.json exists or it names no archive.

Raises:

Type Description
ReleaseError

If targets.json exists but cannot be read.

needs_full_archive staticmethod

needs_full_archive(repo_dir: Path, app_name: str) -> bool

True for the first release after a withdrawal.

tufup builds the patch from the latest archive still in the tree. Clients on a withdrawn version above it hold another archive: the patch would fail its hash check, so the release ships without one.

same_version staticmethod

same_version(left: str, right: str) -> bool

Compare two version strings by PEP 440 meaning, not spelling.

1.2.3-rc.1 and 1.2.3rc1 are the same version. Strings that are not PEP 440 versions fall back to an exact comparison.