Command line

This documents the command-line interface (CLI) and its Python counterparts, including some function signatures and explanation of the choices we’ve made. This document is primarily to help us (the developers) stay focused on the package’s scope.

We use symbols to indicate the status of implementation for the different parts of the interface (see table below). For work that is planned or is in progress, we include in-depth descriptions of the planned implementation. This may include signatures, docstrings, and pseudocode to clarify the design. Once the interface is implemented (done), we will remove the signatures from the documentation and point to the reference documentation instead. The symbols we use are described in the table below.

A table showing the symbols used to indicate the status of interface components, along with their descriptions.
Status Description

Interface that has been implemented.

Interface that is currently being worked on.

Interface that is planned, but isn’t being worked on currently.

init

Terminal
zen-do init --verbose

This will create an empty .zenodo.toml file in the working directory that contains all the metadata fields available for a Zenodo deposit, but with no values filled in, excluding a partially filled in URN for the project. This simple command is helpful to get started with making a new Zenodo deposit, as well as standardizing the name and structure of the file.

@app.command()
def init(verbose: bool = False) -> None:
    """Create an empty `.zenodo.toml` file that has all the metadata fields.

    Args:
        verbose: Whether to print a log of the actions done.
    """
    # Check if .zenodo.toml already exists
    # check_exists(".zenodo.toml")
    # Copy the template .zenodo.toml file to the working directory
    # OR...
    # Take an internal class of the metadata fields and convert into TOML
    print_if_verbose(verbose, "Created an empty `.zenodo.toml` file.")

list

Terminal
zen-do list --sandbox

The list command fetches the stored Zenodo token and gets all Zenodo deposits listed in the account connected to that token. Both the CLI and the internal Python interface are used in other functions like publish() to find whether a deposit exists or not.

The Python interface, with some implementation comments and docstring, will be:

@app.command()
def list(sandbox: bool = False) -> None:
    """List all Zenodo deposits in an account as raw JSON (from the Zenodo servers).

    Args:
        sandbox: Whether to use the Zenodo sandbox environment for testing purposes.
    """
    # Fetch token from system keyring or environment variable.
    # token: Token = get_token(sandbox=sandbox)
    # Get request to Zenodo servers.
    # TODO: Either dict or a custom class for the deposit, e.g. `Deposit`?
    # deposits: list[Deposit] = list_deposits(sandbox=sandbox, token=token)
    pretty_print(deposits)

get

Terminal
zen-do get [METADATA_FILE] --sandbox

The get command will get the Zenodo deposit JSON based on the metadata file provided. If the deposit doesn’t exist, it will output a message about not finding a match. This is useful for checking if a deposit already exists for a given set of metadata, and if so, getting the deposit information to use for updating or publishing it. Internally uses the Python functions within list() to get the list of deposits and uses that to find the deposit that matches the content of the metadata file using the URN ID.

The Python interface, with some implementation comments and docstring, will be:

@app.command()
def get(metadata_file: Path = Path(".zenodo.toml"), \, *, sandbox: bool = False) -> None:
    """Get the Zenodo deposit JSON based on the metadata file.

    Args:
        metadata_file: The path to the metadata file.
        sandbox: Whether to use the Zenodo sandbox environment for testing purposes.
    """
    # Read metadata file (checking if the URN exists or not).
    # metadata: Metadata = read_metadata(path=metadata_file)
    # Fetch token from system keyring or environment variable.
    # token: Token = get_token(sandbox=sandbox)
    # Get request to Zenodo servers.
    # TODO: Either dict or a custom class for the deposit, e.g. `Deposit`?
    # deposits: list[Deposit] = list_deposits(sandbox=sandbox, token=token)
    # deposit: Deposit = find_deposit(deposits=deposits, metadata=metadata)
    pretty_print(deposit)

convert

Terminal
zen-do convert [METADATA_FILE] --to [FORMAT]

There are many different file formats and standards that contain similar or even identical metadata, but that are structured slightly differently. The convert command helps to keep one “source of truth” between these different formats for those cases where a repository might need different formats for different purposes. For example, GitHub will look for a CITATION.cff file to display the citation information for a repository, but doesn’t recognize other formats (like .zenodo.toml). Rather than manually keeping these different files in sync, the convert command can simplify this syncing task.

We decided to only convert from .zenodo.toml to other formats, rather than allow any format to be converted to any other format because we want the .zenodo.toml file to be the “single source of truth” for a project’s metadata. This means that any changes to the metadata must be done in the .zenodo.toml file and convert can update the other formats. This also simplifies the implementation and testing of the convert command, as we only need to implement the logic for converting from .zenodo.toml to other formats, rather than having to handle conversions between all possible pairs of formats.

There are several different formats that we’ve encountered that would be useful to keep synchronized with the .zenodo.toml file, including:

  • .zenodo.json: This is the file format that Zenodo looks for in their existing GitHub release to record integration. Both the JSON and TOML formats are equivalent in content.
  • CITATION.cff: GitHub uses this file to display the citation information for a repository when it is in the root of the repository. This contains only a subset of the metadata found in the .zenodo.toml file.
  • pyproject.toml: While this file is almost entirely used only for Python projects, it contains similar metadata fields to the .zenodo.toml file, such as the project name, description, authors, and license.
  • codemeta.json: This file is used to describe software metadata in a machine-readable format, following the CodeMeta standard. It contains similar metadata fields to the .zenodo.toml file.
  • _quarto.yml: This file is used to build Quarto documents. Like pyproject.toml, it also contains similar metadata fields to the .zenodo.toml file, such as the title, description, and authors.
  • DESCRIPTION: This is a typical file found in R projects. It is necessary for developing and building R packages. As with pyproject.toml, it contains similar metadata fields to the .zenodo.toml file, such as the package name, description, authors, and license.

The --to argument can be a single format or an array of formats, allowing users to convert to multiple formats in one command.

from cyclopts import App, Parameter
from pathlib import Path
from typing import Annotated
from enum import StrEnum

class Format(StrEnum):
    zenodo_json = ".zenodo.json"
    citation_cff = "CITATION.cff"
    pyproject_toml = "pyproject.toml"
    quarto_yml = "_quarto.yml"
    description = "DESCRIPTION"

@app.command()
def convert(
        metadata_file: Path = Path(".zenodo.toml"),
        /,
        *,
        to: Annotated[list[Format], Parameter(consume_multiple=True)],
        verbose: bool = False
    ) -> None:
    """Convert a metadata file, e.g. `.zenodo.toml`, to other formats.

    Args:
        metadata_file: The path to the metadata file to convert.
        to: The formats to convert the metadata file to. Can be a single format
            or an array of formats.
        verbose: Whether to print a log of the actions done.
    """
    # Read metadata file (checking if the URN exists or not).
    # metadata: Metadata = read_metadata(path=metadata_file)
    # Convert to the given formats
    # formats: list[str] = convert_to_formats(metadata, to)
    # TODO: Consider how updating existing files will work, need to read other formats first?
    # output_path: list[Path] = write_formats(formats)
    print_if_verbose(verbose, f"Written the format(s) to files {output_paths}.")