/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.python;
import org.apache.commons.lang.StringUtils;
import org.apache.zeppelin.interpreter.*;
import org.apache.zeppelin.interpreter.InterpreterResult.Code;
import org.apache.zeppelin.interpreter.InterpreterResult.Type;
import org.apache.zeppelin.scheduler.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Conda support
*/
public class PythonCondaInterpreter extends Interpreter {
Logger logger = LoggerFactory.getLogger(PythonCondaInterpreter.class);
public static final String ZEPPELIN_PYTHON = "zeppelin.python";
public static final String CONDA_PYTHON_PATH = "/bin/python";
public static final String DEFAULT_ZEPPELIN_PYTHON = "python";
public static final Pattern PATTERN_OUTPUT_ENV_LIST = Pattern.compile("([^\\s]*)[\\s*]*\\s(.*)");
public static final Pattern PATTERN_COMMAND_ENV_LIST = Pattern.compile("env\\s*list\\s?");
public static final Pattern PATTERN_COMMAND_ENV = Pattern.compile("env\\s*(.*)");
public static final Pattern PATTERN_COMMAND_LIST = Pattern.compile("list");
public static final Pattern PATTERN_COMMAND_CREATE = Pattern.compile("create\\s*(.*)");
public static final Pattern PATTERN_COMMAND_ACTIVATE = Pattern.compile("activate\\s*(.*)");
public static final Pattern PATTERN_COMMAND_DEACTIVATE = Pattern.compile("deactivate");
public static final Pattern PATTERN_COMMAND_INSTALL = Pattern.compile("install\\s*(.*)");
public static final Pattern PATTERN_COMMAND_UNINSTALL = Pattern.compile("uninstall\\s*(.*)");
public static final Pattern PATTERN_COMMAND_HELP = Pattern.compile("help");
public static final Pattern PATTERN_COMMAND_INFO = Pattern.compile("info");
public PythonCondaInterpreter(Properties property) {
super(property);
}
@Override
public void open() {
}
@Override
public void close() {
}
@Override
public InterpreterResult interpret(String st, InterpreterContext context) {
InterpreterOutput out = context.out;
Matcher activateMatcher = PATTERN_COMMAND_ACTIVATE.matcher(st);
Matcher createMatcher = PATTERN_COMMAND_CREATE.matcher(st);
Matcher installMatcher = PATTERN_COMMAND_INSTALL.matcher(st);
Matcher uninstallMatcher = PATTERN_COMMAND_UNINSTALL.matcher(st);
Matcher envMatcher = PATTERN_COMMAND_ENV.matcher(st);
try {
if (PATTERN_COMMAND_ENV_LIST.matcher(st).matches()) {
String result = runCondaEnvList();
return new InterpreterResult(Code.SUCCESS, Type.HTML, result);
} else if (envMatcher.matches()) {
// `envMatcher` should be used after `listEnvMatcher`
String result = runCondaEnv(getRestArgsFromMatcher(envMatcher));
return new InterpreterResult(Code.SUCCESS, Type.HTML, result);
} else if (PATTERN_COMMAND_LIST.matcher(st).matches()) {
String result = runCondaList();
return new InterpreterResult(Code.SUCCESS, Type.HTML, result);
} else if (createMatcher.matches()) {
String result = runCondaCreate(getRestArgsFromMatcher(createMatcher));
return new InterpreterResult(Code.SUCCESS, Type.HTML, result);
} else if (activateMatcher.matches()) {
String envName = activateMatcher.group(1).trim();
return runCondaActivate(envName);
} else if (PATTERN_COMMAND_DEACTIVATE.matcher(st).matches()) {
return runCondaDeactivate();
} else if (installMatcher.matches()) {
String result = runCondaInstall(getRestArgsFromMatcher(installMatcher));
return new InterpreterResult(Code.SUCCESS, Type.HTML, result);
} else if (uninstallMatcher.matches()) {
String result = runCondaUninstall(getRestArgsFromMatcher(uninstallMatcher));
return new InterpreterResult(Code.SUCCESS, Type.HTML, result);
} else if (st == null || PATTERN_COMMAND_HELP.matcher(st).matches()) {
runCondaHelp(out);
return new InterpreterResult(Code.SUCCESS);
} else if (PATTERN_COMMAND_INFO.matcher(st).matches()) {
String result = runCondaInfo();
return new InterpreterResult(Code.SUCCESS, Type.HTML, result);
} else {
return new InterpreterResult(Code.ERROR, "Not supported command: " + st);
}
} catch (RuntimeException | IOException | InterruptedException e) {
throw new InterpreterException(e);
}
}
private void changePythonEnvironment(String envName)
throws IOException, InterruptedException {
PythonInterpreter python = getPythonInterpreter();
String binPath = null;
if (envName == null) {
binPath = getProperty(ZEPPELIN_PYTHON);
if (binPath == null) {
binPath = DEFAULT_ZEPPELIN_PYTHON;
}
} else {
Map<String, String> envList = getCondaEnvs();
for (String name : envList.keySet()) {
if (envName.equals(name)) {
binPath = envList.get(name) + CONDA_PYTHON_PATH;
break;
}
}
}
python.setPythonCommand(binPath);
}
private void restartPythonProcess() {
PythonInterpreter python = getPythonInterpreter();
python.close();
python.open();
}
protected PythonInterpreter getPythonInterpreter() {
LazyOpenInterpreter lazy = null;
PythonInterpreter python = null;
Interpreter p =
getInterpreterInTheSameSessionByClassName(PythonInterpreter.class.getName());
while (p instanceof WrappedInterpreter) {
if (p instanceof LazyOpenInterpreter) {
lazy = (LazyOpenInterpreter) p;
}
p = ((WrappedInterpreter) p).getInnerInterpreter();
}
python = (PythonInterpreter) p;
if (lazy != null) {
lazy.open();
}
return python;
}
public static String runCondaCommandForTextOutput(String title, List<String> commands)
throws IOException, InterruptedException {
String result = runCommand(commands);
return wrapCondaBasicOutputStyle(title, result);
}
private String runCondaCommandForTableOutput(String title, List<String> commands)
throws IOException, InterruptedException {
StringBuilder sb = new StringBuilder();
String result = runCommand(commands);
// use table output for pretty output
Map<String, String> envPerName = parseCondaCommonStdout(result);
return wrapCondaTableOutputStyle(title, envPerName);
}
protected Map<String, String> getCondaEnvs()
throws IOException, InterruptedException {
String result = runCommand("conda", "env", "list");
Map<String, String> envList = parseCondaCommonStdout(result);
return envList;
}
private String runCondaEnvList() throws IOException, InterruptedException {
return wrapCondaTableOutputStyle("Environment List", getCondaEnvs());
}
private String runCondaEnv(List<String> restArgs)
throws IOException, InterruptedException {
restArgs.add(0, "conda");
restArgs.add(1, "env");
restArgs.add(3, "--yes"); // --yes should be inserted after command
return runCondaCommandForTextOutput(null, restArgs);
}
private InterpreterResult runCondaActivate(String envName)
throws IOException, InterruptedException {
if (null == envName || envName.isEmpty()) {
return new InterpreterResult(Code.ERROR, "Env name should be specified");
}
changePythonEnvironment(envName);
restartPythonProcess();
return new InterpreterResult(Code.SUCCESS, "'" + envName + "' is activated");
}
private InterpreterResult runCondaDeactivate()
throws IOException, InterruptedException {
changePythonEnvironment(null);
restartPythonProcess();
return new InterpreterResult(Code.SUCCESS, "Deactivated");
}
private String runCondaList() throws IOException, InterruptedException {
List<String> commands = new ArrayList<String>();
commands.add("conda");
commands.add("list");
return runCondaCommandForTableOutput("Installed Package List", commands);
}
private void runCondaHelp(InterpreterOutput out) {
try {
out.setType(InterpreterResult.Type.HTML);
out.writeResource("output_templates/conda_usage.html");
} catch (IOException e) {
logger.error("Can't print usage", e);
}
}
private String runCondaInfo() throws IOException, InterruptedException {
List<String> commands = new ArrayList<String>();
commands.add("conda");
commands.add("info");
return runCondaCommandForTextOutput("Conda Information", commands);
}
private String runCondaCreate(List<String> restArgs)
throws IOException, InterruptedException {
restArgs.add(0, "conda");
restArgs.add(1, "create");
restArgs.add(2, "--yes");
return runCondaCommandForTextOutput("Environment Creation", restArgs);
}
private String runCondaInstall(List<String> restArgs)
throws IOException, InterruptedException {
restArgs.add(0, "conda");
restArgs.add(1, "install");
restArgs.add(2, "--yes");
return runCondaCommandForTextOutput("Package Installation", restArgs);
}
private String runCondaUninstall(List<String> restArgs)
throws IOException, InterruptedException {
restArgs.add(0, "conda");
restArgs.add(1, "uninstall");
restArgs.add(2, "--yes");
return runCondaCommandForTextOutput("Package Uninstallation", restArgs);
}
public static String wrapCondaBasicOutputStyle(String title, String content) {
StringBuilder sb = new StringBuilder();
if (null != title && !title.isEmpty()) {
sb.append("<h4>").append(title).append("</h4>\n")
.append("</div><br />\n");
}
sb.append("<div style=\"white-space:pre-wrap;\">\n")
.append(content)
.append("</div>");
return sb.toString();
}
public static String wrapCondaTableOutputStyle(String title, Map<String, String> kv) {
StringBuilder sb = new StringBuilder();
if (null != title && !title.isEmpty()) {
sb.append("<h4>").append(title).append("</h4>\n");
}
sb.append("<div style=\"display:table;white-space:pre-wrap;\">\n");
for (String name : kv.keySet()) {
String path = kv.get(name);
sb.append(String.format("<div style=\"display:table-row\">" +
"<div style=\"display:table-cell;width:150px\">%s</div>" +
"<div style=\"display:table-cell;\">%s</div>" +
"</div>\n",
name, path));
}
sb.append("</div>\n");
return sb.toString();
}
public static Map<String, String> parseCondaCommonStdout(String out)
throws IOException, InterruptedException {
Map<String, String> kv = new LinkedHashMap<String, String>();
String[] lines = out.split("\n");
for (String s : lines) {
if (s == null || s.isEmpty() || s.startsWith("#")) {
continue;
}
Matcher match = PATTERN_OUTPUT_ENV_LIST.matcher(s);
if (!match.matches()) {
continue;
}
kv.put(match.group(1), match.group(2));
}
return kv;
}
@Override
public void cancel(InterpreterContext context) {
}
@Override
public FormType getFormType() {
return FormType.NONE;
}
@Override
public int getProgress(InterpreterContext context) {
return 0;
}
/**
* Use python interpreter's scheduler.
* To make sure %python.conda paragraph and %python paragraph runs sequentially
*/
@Override
public Scheduler getScheduler() {
PythonInterpreter pythonInterpreter = getPythonInterpreter();
if (pythonInterpreter != null) {
return pythonInterpreter.getScheduler();
} else {
return null;
}
}
public static String runCommand(List<String> commands)
throws IOException, InterruptedException {
StringBuilder sb = new StringBuilder();
ProcessBuilder builder = new ProcessBuilder(commands);
builder.redirectErrorStream(true);
Process process = builder.start();
InputStream stdout = process.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
sb.append("\n");
}
int r = process.waitFor(); // Let the process finish.
if (r != 0) {
throw new RuntimeException("Failed to execute `" +
StringUtils.join(commands, " ") + "` exited with " + r);
}
return sb.toString();
}
public static String runCommand(String ... command)
throws IOException, InterruptedException {
List<String> list = new ArrayList<>(command.length);
for (String arg : command) {
list.add(arg);
}
return runCommand(list);
}
public static List<String> getRestArgsFromMatcher(Matcher m) {
// Arrays.asList just returns fixed-size, so we should use ctor instead of
return new ArrayList<>(Arrays.asList(m.group(1).split(" ")));
}
}