Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[python] Add CLI command to run tests #680

Merged
merged 1 commit into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
package ai.langstream.cli.commands;

import ai.langstream.cli.commands.python.LoadPythonDependenciesCmd;
import ai.langstream.cli.commands.python.PythonRunTests;
import lombok.Getter;
import picocli.CommandLine;

@CommandLine.Command(
name = "python",
header = "Tools for Python developers",
subcommands = {LoadPythonDependenciesCmd.class})
subcommands = {LoadPythonDependenciesCmd.class, PythonRunTests.class})
@Getter
public class RootPythonCmd {
@CommandLine.ParentCommand private RootCmd rootCmd;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@
import ai.langstream.cli.commands.BaseCmd;
import ai.langstream.cli.commands.RootCmd;
import ai.langstream.cli.commands.RootPythonCmd;
import ai.langstream.cli.util.DockerImageUtils;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import lombok.SneakyThrows;
import org.apache.commons.io.input.Tailer;
import org.apache.commons.io.input.TailerListener;
import picocli.CommandLine;

public abstract class BasePythonCmd extends BaseCmd {
Expand All @@ -28,4 +41,168 @@ public abstract class BasePythonCmd extends BaseCmd {
protected RootCmd getRootCmd() {
return rootPythonCmd.getRootCmd();
}

protected static final AtomicReference<ProcessHandle> dockerProcess = new AtomicReference<>();

@CommandLine.Option(
names = {"-app", "--application"},
description = "Application directory path",
required = true)
protected String appPath;

@CommandLine.Option(
names = {"--docker-args"},
description = "Additional docker arguments")
protected List<String> dockerAdditionalArgs = new ArrayList<>();

@CommandLine.Option(
names = {"--docker-command"},
description = "Command to run docker")
protected String dockerCommand = "docker";

@CommandLine.Option(
names = {"--langstream-runtime-version"},
description = "Version of the LangStream runtime to use",
defaultValue = "${env:LANGSTREAM_RUNTIME_DOCKER_IMAGE_VERSION}")
protected String dockerImageVersion;

@CommandLine.Option(
names = {"--langstream-runtime-docker-image"},
description = "Docker image of the LangStream runtime to use",
defaultValue = "${env:LANGSTREAM_RUNTIME_DOCKER_IMAGE}")
protected String dockerImageName;

protected void cleanEnvironment() {
if (dockerProcess.get() != null) {
dockerProcess.get().destroyForcibly();
}
}

@Override
@SneakyThrows
public void run() {

DockerImageUtils.DockerImage dockerImage =
DockerImageUtils.computeDockerImage(dockerImageVersion, dockerImageName);

if (appPath == null || appPath.isEmpty()) {
throw new IllegalArgumentException("application files are required");
}

final File appDirectory = new File(appPath);

log("Using docker image: " + dockerImage.getFullName());

downloadDependencies(appDirectory.toPath(), getClient(), this::log);

Runtime.getRuntime().addShutdownHook(new Thread(this::cleanEnvironment));

executeOnDocker(appDirectory, dockerImage);
}

private void executeOnDocker(File appDirectory, DockerImageUtils.DockerImage dockerImage)
throws Exception {
final File appTmp = appDirectory;

File pythonDirectory = new File(appDirectory, "python");
if (!pythonDirectory.isDirectory()) {
throw new IllegalArgumentException(
"Directory " + pythonDirectory.getAbsolutePath() + " not found");
}
validatePythonDirectory(pythonDirectory);

String imageName = dockerImage.getFullName();
List<String> commandLine = new ArrayList<>();
commandLine.add(dockerCommand);

/*
docker run --rm \
-v $(pwd):/app-code-download \
--entrypoint "" \
-w /app-code-download/python ghcr.io/langstream/langstream-runtime:0.1.0 \
/bin/bash -c 'pip3 install --target ./lib --upgrade --prefer-binary -r requirements.txt'

*/

commandLine.add("run");
commandLine.add("--rm");
commandLine.add("--entrypoint");
commandLine.add("/bin/bash");
commandLine.add("-w");
commandLine.add("/code/application/python");

commandLine.add("-v");
commandLine.add(appTmp.getAbsolutePath() + ":/code/application");

if (dockerAdditionalArgs != null) {
commandLine.addAll(dockerAdditionalArgs);
}

commandLine.add(imageName);

if (getRootCmd().isVerbose()) {
System.out.println("Executing:");
System.out.println(String.join(" ", commandLine));
}

commandLine.add("-c");

addSpecificCommand(commandLine);

final Path outputLog = Files.createTempFile("langstream", ".log");
log("Logging to file: " + outputLog.toAbsolutePath());
ProcessBuilder processBuilder =
new ProcessBuilder(commandLine)
.redirectErrorStream(true)
.redirectOutput(outputLog.toFile());
Process process = processBuilder.start();
dockerProcess.set(process.toHandle());
CompletableFuture.runAsync(
() -> tailLogSysOut(outputLog), Executors.newSingleThreadExecutor());

final int exited = process.waitFor();
// wait for the log to be printed
Thread.sleep(1000);
if (exited != 0) {
throw new RuntimeException("Process exited with code " + exited);
}
}

protected void validatePythonDirectory(File pythonDirectory) {}

protected void tailLogSysOut(Path outputLog) {

TailerListener listener =
new TailerListener() {
@Override
public void fileNotFound() {}

@Override
public void fileRotated() {}

@Override
public void handle(Exception e) {}

@Override
public void handle(String s) {
log(s);
}

@Override
public void init(Tailer tailer) {}
};
try (final Tailer tailer =
Tailer.builder()
.setTailerListener(listener)
.setStartThread(false)
.setDelayDuration(Duration.ofMillis(100))
.setFile(outputLog.toFile())
.get(); ) {
while (true) {
tailer.run();
}
}
}

protected abstract void addSpecificCommand(List<String> commandLine);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,93 +15,21 @@
*/
package ai.langstream.cli.commands.python;

import ai.langstream.cli.util.DockerImageUtils;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import lombok.SneakyThrows;
import org.apache.commons.io.input.Tailer;
import org.apache.commons.io.input.TailerListener;
import picocli.CommandLine;

@CommandLine.Command(
name = "load-pip-requirements",
header = "Process python dependencies in requirements.txt")
public class LoadPythonDependenciesCmd extends BasePythonCmd {

private static final AtomicReference<ProcessHandle> dockerProcess = new AtomicReference<>();

@CommandLine.Option(
names = {"-app", "--application"},
description = "Application directory path",
required = true)
private String appPath;

@CommandLine.Option(
names = {"--docker-args"},
description = "Additional docker arguments")
private List<String> dockerAdditionalArgs = new ArrayList<>();

@CommandLine.Option(
names = {"--docker-command"},
description = "Command to run docker")
private String dockerCommand = "docker";

@CommandLine.Option(
names = {"--langstream-runtime-version"},
description = "Version of the LangStream runtime to use",
defaultValue = "${env:LANGSTREAM_RUNTIME_DOCKER_IMAGE_VERSION}")
private String dockerImageVersion;

@CommandLine.Option(
names = {"--langstream-runtime-docker-image"},
description = "Docker image of the LangStream runtime to use",
defaultValue = "${env:LANGSTREAM_RUNTIME_DOCKER_IMAGE}")
private String dockerImageName;

@Override
@SneakyThrows
public void run() {

DockerImageUtils.DockerImage dockerImage =
DockerImageUtils.computeDockerImage(dockerImageVersion, dockerImageName);

if (appPath == null || appPath.isEmpty()) {
throw new IllegalArgumentException("application files are required");
}

final File appDirectory = new File(appPath);

log("Using docker image: " + dockerImage.getFullName());

downloadDependencies(appDirectory.toPath(), getClient(), this::log);

Runtime.getRuntime().addShutdownHook(new Thread(this::cleanEnvironment));

executeOnDocker(appDirectory, dockerImage);
}

private void cleanEnvironment() {
if (dockerProcess.get() != null) {
dockerProcess.get().destroyForcibly();
}
protected void addSpecificCommand(List<String> commandLine) {
commandLine.add(
"pip3 install --target ./lib --upgrade --prefer-binary -r requirements.txt");
}

private void executeOnDocker(File appDirectory, DockerImageUtils.DockerImage dockerImage)
throws Exception {
final File appTmp = appDirectory;

File pythonDirectory = new File(appDirectory, "python");
if (!pythonDirectory.isDirectory()) {
throw new IllegalArgumentException(
"Directory " + pythonDirectory.getAbsolutePath() + " not found");
}
protected void validatePythonDirectory(File pythonDirectory) {
File requirementsFile = new File(pythonDirectory, "requirements.txt");
if (!requirementsFile.isFile()) {
throw new IllegalArgumentException(
Expand All @@ -110,95 +38,5 @@ private void executeOnDocker(File appDirectory, DockerImageUtils.DockerImage doc
+ " not found in "
+ pythonDirectory);
}

String imageName = dockerImage.getFullName();
List<String> commandLine = new ArrayList<>();
commandLine.add(dockerCommand);

/*
docker run --rm \
-v $(pwd):/app-code-download \
--entrypoint "" \
-w /app-code-download/python ghcr.io/langstream/langstream-runtime:0.1.0 \
/bin/bash -c 'pip3 install --target ./lib --upgrade --prefer-binary -r requirements.txt'

*/

commandLine.add("run");
commandLine.add("--rm");
commandLine.add("--entrypoint");
commandLine.add("/bin/bash");
commandLine.add("-w");
commandLine.add("/code/application/python");

commandLine.add("-v");
commandLine.add(appTmp.getAbsolutePath() + ":/code/application");

if (dockerAdditionalArgs != null) {
commandLine.addAll(dockerAdditionalArgs);
}

commandLine.add(imageName);

if (getRootCmd().isVerbose()) {
System.out.println("Executing:");
System.out.println(String.join(" ", commandLine));
}

commandLine.add("-c");
commandLine.add(
"pip3 install --target ./lib --upgrade --prefer-binary -r requirements.txt");

final Path outputLog = Files.createTempFile("langstream", ".log");
log("Logging to file: " + outputLog.toAbsolutePath());
ProcessBuilder processBuilder =
new ProcessBuilder(commandLine)
.redirectErrorStream(true)
.redirectOutput(outputLog.toFile());
Process process = processBuilder.start();
dockerProcess.set(process.toHandle());
CompletableFuture.runAsync(
() -> tailLogSysOut(outputLog), Executors.newSingleThreadExecutor());

final int exited = process.waitFor();
// wait for the log to be printed
Thread.sleep(1000);
if (exited != 0) {
throw new RuntimeException("Process exited with code " + exited);
}
}

private void tailLogSysOut(Path outputLog) {

TailerListener listener =
new TailerListener() {
@Override
public void fileNotFound() {}

@Override
public void fileRotated() {}

@Override
public void handle(Exception e) {}

@Override
public void handle(String s) {
log(s);
}

@Override
public void init(Tailer tailer) {}
};
try (final Tailer tailer =
Tailer.builder()
.setTailerListener(listener)
.setStartThread(false)
.setDelayDuration(Duration.ofMillis(100))
.setFile(outputLog.toFile())
.get(); ) {
while (true) {
tailer.run();
}
}
}
}
Loading
Loading