All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.technologybrewery.habushu.exec.PoetryCommandHelper Maven / Gradle / Ivy

Go to download

Leverages Poetry and Pyenv to provide an automated, predictable order of execution of build commands that apply DevOps and configuration management best practices

There is a newer version: 2.17.0
Show newest version
package org.technologybrewery.habushu.exec;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.maven.plugin.MojoExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Facilitates the execution of Poetry commands.
 */
public class PoetryCommandHelper {

    private static final String POETRY_COMMAND = "poetry";
    private static final Logger logger = LoggerFactory.getLogger(PoetryCommandHelper.class);

    private static final String extractVersionRegex = "[^0-9\\.]";

    private File workingDirectory;

    public PoetryCommandHelper(File workingDirectory) {
	this.workingDirectory = workingDirectory;
    }

    /**
     * Returns a {@link Boolean} and {@link String} {@link Pair} indicating whether
     * Poetry is installed and if so, the version of Poetry that is installed. If
     * Poetry is not installed, the returned {@link String} part of the {@link Pair}
     * will be {@code null}.
     * 
     * @return
     */
    public Pair getIsPoetryInstalledAndVersion() {
	try {
	    ProcessExecutor executor = createPoetryExecutor(Arrays.asList("--version"));
	    String versionResult = executor.executeAndGetResult(logger);

	    // Extracts version number from output, whether it's "Poetry version 1.1.15" or
	    // "Poetry (version 1.2.1)"
	    String version = versionResult.replaceAll(extractVersionRegex, "");
	    return new ImmutablePair(true, version);
	} catch (Throwable e) {
	    return new ImmutablePair(false, null);
	}
    }

    /**
     * Returns whether the specified dependency package is installed within this
     * Poetry project's virtual environment (and pyproject.toml).
     * 
     * @param packageName
     * @return
     */
    public boolean isDependencyInstalled(String packageName) {
	try {
	    execute(Arrays.asList("show", packageName));
	} catch (Throwable e) {
	    return false;
	}
	return true;

    }

    /**
     * Installs the specified package as a development dependency to this Poetry
     * project's virtual environment and pyproject.toml specification.
     * 
     * @param packageName
     */
    public void installDevelopmentDependency(String packageName) throws MojoExecutionException {
	execute(Arrays.asList("add", packageName, "--group", "dev"));
    }

    /**
     * Executes a Poetry command with the given arguments, logs the executed
     * command, and returns the resultant process output as a string. This method
     * should be utilized when performing downstream logic based on the output of a
     * Poetry command, or it is desirable to not show the command's generated
     * stdout.
     * 
     * @param arguments
     * @return
     * @throws MojoExecutionException
     */
    public String execute(List arguments) throws MojoExecutionException {
	if (logger.isInfoEnabled()) {
	    logger.info("Executing Poetry command: {} {}", POETRY_COMMAND, StringUtils.join(arguments, " "));
	}
	ProcessExecutor executor = createPoetryExecutor(arguments);
	return executor.executeAndGetResult(logger);
    }

    /**
     * Executes a Poetry command with the given arguments, logs the executed
     * command, logs the stdout/stderr generated by the process, and returns the
     * process exit code. This method should be utilized when it is desirable to
     * immediately show all of the stdout/stderr produced by a Poetry command for
     * diagnostic purposes.
     * 
     * @param arguments
     * @return
     * @throws MojoExecutionException
     */
    public int executeAndLogOutput(List arguments) throws MojoExecutionException {
	if (logger.isInfoEnabled()) {
	    logger.info("Executing Poetry command: {} {}", POETRY_COMMAND, StringUtils.join(arguments, " "));
	}
	ProcessExecutor executor = createPoetryExecutor(arguments);
	return executor.executeAndRedirectOutput(logger);
    }

    /**
     * Similar to {@link #executeAndLogOutput(List)}, except the executed Poetry
     * command that is logged obfuscates/masks any given command arguments that are
     * marked as sensitive. This method should be utilized if any Poetry command
     * line arguments contain sensitive values that are not desirable to log, such
     * as passwords.
     * 
     * @param argAndIsSensitivePairs
     * @return
     * @throws MojoExecutionException
     */
    public int executeWithSensitiveArgsAndLogOutput(List> argAndIsSensitivePairs)
	    throws MojoExecutionException {
	if (logger.isInfoEnabled()) {
	    List argsWithSensitiveArgsMasked = argAndIsSensitivePairs.stream()
		    .map(pair -> pair.getRight() ? "XXXX" : pair.getLeft()).collect(Collectors.toList());
	    logger.info("Executing Poetry command: {} {}", POETRY_COMMAND,
		    StringUtils.join(argsWithSensitiveArgsMasked, " "));
	}
	ProcessExecutor executor = createPoetryExecutor(
		argAndIsSensitivePairs.stream().map(Pair::getLeft).collect(Collectors.toList()));
	return executor.executeAndRedirectOutput(logger);
    }

    /**
     * Executes a Poetry command with the given arguments and logs a warning message
     * if the command has not yet completed after the specified timeout period. This
     * may be useful for providing input to developers when certain Poetry commands
     * are running for longer than expected and may need to be manually halted due
     * to cache-related issues.
* NOTE:The executed Poetry command will *not* be halted nor terminated * when the timeout expires. After the timeout expires, this method will * continue to wait until underlying Poetry command completes. * * @param arguments * @param timeout * @param timeUnit * @return */ public Integer executePoetryCommandAndLogAfterTimeout(List arguments, int timeout, TimeUnit timeUnit) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future future = executor.submit(() -> this.executeAndLogOutput(arguments)); try { return future.get(timeout, timeUnit); } catch (TimeoutException e) { logger.warn("poetry " + String.join(" ", arguments) + " has been running for quite some time, you may want to quit the mvn process (Ctrl+c) and run \"poetry cache clear . --all\" and restart your build."); try { return future.get(); } catch (InterruptedException | ExecutionException e1) { throw new RuntimeException("Error occurred while waiting for Poetry command to complete", e1); } } catch (Exception e) { throw new RuntimeException(String.format("Error occurred while performing Poetry command: poetry %s", StringUtils.join(arguments, " ")), e); } finally { executor.shutdown(); } } /** * Installs a Poetry plugin with the given name. * * @param name * @return * @throws MojoExecutionException */ public int installPoetryPlugin(String name) throws MojoExecutionException { List args = new ArrayList(); args.add("self"); args.add("add"); args.add(name); return this.executeAndLogOutput(args); } protected ProcessExecutor createPoetryExecutor(List arguments) { List fullCommandArgs = new ArrayList<>(); fullCommandArgs.add(POETRY_COMMAND); fullCommandArgs.addAll(arguments); return new ProcessExecutor(workingDirectory, fullCommandArgs, Platform.guess(), null); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy