Index: javavenv/src/main/java/geniusweb/javavenv/Call.java
===================================================================
--- javavenv/src/main/java/geniusweb/javavenv/Call.java	(revision 1)
+++ javavenv/src/main/java/geniusweb/javavenv/Call.java	(revision 1)
@@ -0,0 +1,138 @@
+package geniusweb.javavenv;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A straight python call. Handles all the connection stuff: connecting to the
+ * input and output streams so that we can try to follow what python is doing.
+ * NOTE generally this is for internal use, instead use {@link PythonVenv}
+ */
+public class Call {
+	private final List<String> allargs = new LinkedList<>();
+	// size 2, we expect both stdin and stderr to always post something here.
+	private final BlockingQueue<Object> result = new ArrayBlockingQueue<Object>(
+			2);
+	private final int deadlinems;
+
+	/**
+	 * Call python with given arguments. call {@link #run()} to run. We
+	 * recommend thatn you use {@link PythonVenv} to run commands instead of
+	 * this direct call.
+	 * 
+	 * @param pythonexe  a working python3 exe, typically
+	 *                   "/usr/local/bin/python3" or something in a venv on temp
+	 *                   disk.
+	 * @param args       the other args for the call
+	 * @param deadlinems the deadline (ms) after which the call should be
+	 *                   cancelled.
+	 */
+
+	public Call(String pythonexe, List<String> args, int deadlinems) {
+		if (pythonexe == null || args == null)
+			throw new IllegalArgumentException("args must be not null");
+		allargs.add(pythonexe);
+		allargs.addAll(args);
+		this.deadlinems = deadlinems;
+	}
+
+	/**
+	 * 
+	 * @return the result of the python call (that is, what the program printed
+	 *         out)
+	 * @throws InterruptedException if the call does not terminate in time.
+	 * @throws IOException          if we can not connect properly to the python
+	 *                              process
+	 * @throws PythonError          if the python program printed to stderr, eg
+	 *                              when an error occured. We try to capture all
+	 *                              the error lines as text in the pythonerror
+	 *                              object.
+	 */
+	public String run() throws InterruptedException, IOException, PythonError {
+		ProcessBuilder processBuilder = new ProcessBuilder(allargs);
+		// System.out.println("all args " + allargs);
+		Process p;
+		p = processBuilder.start();
+
+		Runnable stdinreader = new Runnable() {
+			@Override
+			public void run() {
+				String totalreply = "";
+				byte[] cbuf = new byte[500];
+				InputStream is = p.getInputStream();
+				try {
+					while (true) {
+						int n = is.read(cbuf);
+						if (n == -1)
+							break;
+						totalreply += new String(cbuf, 0, n, "UTF8");
+					}
+					result.add(totalreply);
+				} catch (IOException e) {
+					result.add(e);
+				}
+			}
+		};
+
+		Runnable errorlogger = new Runnable() {
+			@Override
+			public void run() {
+				byte[] cbuf = new byte[500];
+				InputStream is = p.getErrorStream();
+				String totalerror = "";
+				try {
+					while (true) {
+						int n = is.read(cbuf);
+						if (n == -1)
+							break;
+						totalerror += new String(cbuf, 0, n, "UTF8");
+					}
+					if (!totalerror.isEmpty())
+						result.add(new PythonError(totalerror));
+					else
+						result.add("");
+				} catch (IOException e) {
+					result.add(e);
+				}
+			}
+
+		};
+		new Thread(stdinreader).start();
+		new Thread(errorlogger).start();
+
+		// now we wait till both are finished.
+		// wait till both are finished. Order is irrelevant.
+		Object res1 = result.poll(deadlinems, TimeUnit.MILLISECONDS);
+		if (res1 == null)
+			throw new InterruptedException("Python code failed to return");
+		Object res2 = result.poll(100, TimeUnit.MILLISECONDS);
+		if (res2 == null)
+			throw new InterruptedException(
+					"Second python channel failed to close");
+		// If there is a non-empty error, return it, otherwise return res
+		rethrowIfException(res1);
+		rethrowIfException(res2);
+		if (!((String) res1).isEmpty())
+			return (String) res1;
+		return (String) res2;
+
+	}
+
+	@Override
+	public String toString() {
+		return "python " + allargs;
+	}
+
+	private void rethrowIfException(Object obj)
+			throws IOException, PythonError {
+		if (obj instanceof Exception) {
+			throw new PythonError("Failed to execute " + this, (Exception) obj);
+		}
+	}
+
+}
Index: javavenv/src/main/java/geniusweb/javavenv/PartyProxy.java
===================================================================
--- javavenv/src/main/java/geniusweb/javavenv/PartyProxy.java	(revision 15)
+++ javavenv/src/main/java/geniusweb/javavenv/PartyProxy.java	(revision 1)
@@ -12,6 +12,4 @@
 import geniusweb.party.Party;
 import tudelft.utilities.logging.Reporter;
-import tudelft.utilities.pyrunner.Pipe;
-import tudelft.utilities.pyrunner.PythonVenv;
 
 /**
Index: javavenv/src/main/java/geniusweb/javavenv/Pipe.java
===================================================================
--- javavenv/src/main/java/geniusweb/javavenv/Pipe.java	(revision 1)
+++ javavenv/src/main/java/geniusweb/javavenv/Pipe.java	(revision 1)
@@ -0,0 +1,189 @@
+package geniusweb.javavenv;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Level;
+
+import tudelft.utilities.listener.DefaultListenable;
+import tudelft.utilities.logging.Reporter;
+
+/**
+ * A pipe to a python program. The python program is only created after run is
+ * called, so that you can first add your listener
+ */
+public class Pipe extends DefaultListenable<String> {
+	protected static final int MAX_BLOCK_SIZE = 20000000;
+	private final List<String> allargs = new LinkedList<>();
+	private final int deadlinems;
+
+	// set when ween counter an error
+	private transient Exception exception = null;
+	private transient PythonError pythonerror = null;
+	// set when process is started
+	private transient OutputStream out = null;
+	Process p = null;
+	private Reporter logger;
+
+	/**
+	 * Call python with given arguments. call {@link #run()} to run.
+	 * 
+	 * @param pythonexe  a working python3 exe, typically
+	 *                   "/usr/local/bin/python3" or something in a venv on temp
+	 *                   disk.
+	 * @param args       the other args for the call. The args must make up a
+	 *                   command that puts the python program into a
+	 *                   stdin-stdout-stderr pipe mode so that we can create a
+	 *                   pipe.
+	 * @param deadlinems the deadline (ms) after which the connection shoubd
+	 *                   have been created. FIXME IGNORED CURRENTLY.
+	 * @param logger     the {@link Reporter} for the associated party
+	 */
+	public Pipe(String pythonexe, List<String> args, int deadlinems,
+			Reporter logger) {
+		if (pythonexe == null || args == null || logger == null)
+			throw new IllegalArgumentException("args must be not null");
+		allargs.add(pythonexe);
+		allargs.addAll(args);
+		this.deadlinems = deadlinems;
+		this.logger = logger;
+	}
+
+	/**
+	 * Send data down the pipe
+	 * 
+	 * @param data the data to send
+	 * @throws IOException if the pipe broke
+	 */
+	public void send(String data) throws IOException {
+		// System.out.println("Sending " + data);
+		byte[] lenbuf = new byte[4];
+		ByteBuffer bb = ByteBuffer.wrap(lenbuf);
+		bb.order(ByteOrder.LITTLE_ENDIAN);
+		bb.putInt(data.length());
+//		System.out.println("lenbuf=" + lenbuf[0] + "," + lenbuf[1] + ","
+//				+ lenbuf[2] + "," + lenbuf[3]);
+
+		out.write(lenbuf);
+		out.write(data.getBytes("UTF8"));
+		out.flush();
+	}
+
+	/**
+	 * start the process and connect the pipe. Returns immediately after opening
+	 * the channels to the party.
+	 * 
+	 * @throws IOException if we can not start up the process
+	 */
+	public void run() throws IOException {
+		ProcessBuilder processBuilder = new ProcessBuilder(allargs);
+		// System.out.println("all args " + allargs);
+		p = processBuilder.start();
+		out = p.getOutputStream();
+
+		Runnable stdinreader = new Runnable() {
+			@Override
+			public void run() {
+
+				String totalreply = "";
+				byte[] cbuf = new byte[500];
+				InputStream is = p.getInputStream();
+				try {
+					while (true) {
+						int n = is.read(cbuf, 0, 4);
+						if (n != 4)
+							break;
+						ByteBuffer bb = ByteBuffer.wrap(cbuf);
+						bb.order(ByteOrder.LITTLE_ENDIAN);
+						int remaining = bb.getInt();
+						if (remaining > MAX_BLOCK_SIZE)
+							throw new IOException("Stdin data exceeds "
+									+ MAX_BLOCK_SIZE
+									+ " bytes. Assuming corrupted input stream");
+						// System.out.println("Received header for " +
+						// remaining);
+						if (remaining == 0)
+							break;
+						while (remaining > 0) {
+							n = is.read(cbuf);
+							if (n == -1)
+								break;
+							totalreply += new String(cbuf, 0, n, "UTF8");
+							remaining -= n;
+						}
+						notifyListeners(totalreply);
+						totalreply = "";
+					}
+				} catch (IOException e) {
+					e.printStackTrace();
+					if (exception == null)
+						exception = e;
+				}
+				logger.log(Level.INFO, "Input pipe from party closed.");
+				// Final null to trigger tell we ended
+				notifyListeners(null);
+			}
+		};
+
+		/**
+		 * stderr is not so strictly controlled by us. Python may print at any
+		 * time to this. We assume that the party will exit shortly after the
+		 * exception and that the pipe will break then. We just accumulate and
+		 * store the error
+		 */
+		Runnable errorlogger = new Runnable() {
+			@Override
+			public void run() {
+				byte[] cbuf = new byte[500];
+				InputStream is = p.getErrorStream();
+				String totalerror = "";
+				try {
+					while (true) {
+						int n = is.read(cbuf);
+						if (n == -1)
+							break;
+						String str = new String(cbuf, 0, n, "UTF8");
+						logger.log(Level.WARNING, str);
+						totalerror += str;
+					}
+					if (!totalerror.isEmpty())
+						pythonerror = new PythonError(totalerror);
+				} catch (IOException e) {
+					if (exception == null)
+						exception = e;
+				}
+			}
+
+		};
+
+		new Thread(stdinreader).start();
+		new Thread(errorlogger).start();
+	}
+
+	/**
+	 * 
+	 * @return an error, either picked up from stderr (if the party prints some
+	 *         error), or if that is null, from our own exception handling.
+	 *         Returns null if both are not set.
+	 */
+	public Exception getError() {
+		if (pythonerror != null)
+			return pythonerror;
+		return exception;
+	}
+
+	/**
+	 * Terminate the pipes. Kill the party.
+	 */
+	public void terminate() {
+		if (p != null) {
+			p.destroyForcibly();
+			// that should also terminate the sockets.
+			p = null;
+		}
+	}
+}
Index: javavenv/src/main/java/geniusweb/javavenv/PythonError.java
===================================================================
--- javavenv/src/main/java/geniusweb/javavenv/PythonError.java	(revision 1)
+++ javavenv/src/main/java/geniusweb/javavenv/PythonError.java	(revision 1)
@@ -0,0 +1,18 @@
+package geniusweb.javavenv;
+
+/**
+ * Contains an error message text from the python interpreter
+ */
+public class PythonError extends Exception {
+
+	private static final long serialVersionUID = 4602563752640840127L;
+
+	public PythonError(String msg) {
+		super(msg);
+	}
+
+	public PythonError(String msg, Throwable cause) {
+		super(msg, cause);
+	}
+
+}
Index: javavenv/src/main/java/geniusweb/javavenv/PythonPartyFactory.java
===================================================================
--- javavenv/src/main/java/geniusweb/javavenv/PythonPartyFactory.java	(revision 15)
+++ javavenv/src/main/java/geniusweb/javavenv/PythonPartyFactory.java	(revision 1)
@@ -13,7 +13,4 @@
 import geniusweb.party.Capabilities;
 import tudelft.utilities.logging.ReportToLogger;
-import tudelft.utilities.pyrunner.Pipe;
-import tudelft.utilities.pyrunner.PythonError;
-import tudelft.utilities.pyrunner.PythonVenv;
 
 /**
@@ -25,6 +22,6 @@
 public class PythonPartyFactory {
 
-	private static final int PARTY_MAX_INSTALL_TIME = 180000; // ms
-	private static final int PIP_MAX_UPGRADE_TIME = 90000; // ms
+	private static final int PARTY_MAX_INSTALL_TIME = 60000; // ms
+	private static final int PIP_MAX_UPGRADE_TIME = 30000; // ms
 	private final ObjectMapper jackson = new ObjectMapper();
 	private final File targz;
@@ -92,6 +89,4 @@
 		PythonVenv env = new PythonVenv(name, logger);
 		env.call("-m pip install --upgrade pip", PIP_MAX_UPGRADE_TIME);
-		// #39 without wheel you now get warnings
-		env.call("-m pip install wheel", PIP_MAX_UPGRADE_TIME);
 		env.call(Arrays.asList("-m", "pip", "install", targz.getAbsolutePath()),
 				PARTY_MAX_INSTALL_TIME);
@@ -104,5 +99,5 @@
 		String capstr = venv.call(Arrays.asList("-c",
 				"from geniusweb.partystdio.PartyStdIo import PartyStdIo; import party; PartyStdIo(party.party()).capabilities()"),
-				1500);
+				500);
 		logger.log(Level.INFO, "Capabilities=" + capstr);
 		return jackson.readValue(capstr, Capabilities.class);
@@ -113,5 +108,5 @@
 		String capstr = venv.call(Arrays.asList("-c",
 				"from geniusweb.partystdio.PartyStdIo import PartyStdIo; import party; PartyStdIo(party.party()).description()"),
-				1500);
+				500);
 		logger.log(Level.INFO, "Description=" + capstr);
 		return capstr;
Index: javavenv/src/main/java/geniusweb/javavenv/PythonVenv.java
===================================================================
--- javavenv/src/main/java/geniusweb/javavenv/PythonVenv.java	(revision 1)
+++ javavenv/src/main/java/geniusweb/javavenv/PythonVenv.java	(revision 1)
@@ -0,0 +1,161 @@
+package geniusweb.javavenv;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Level;
+
+import org.apache.commons.io.FileUtils;
+
+import tudelft.utilities.logging.Reporter;
+
+/**
+ * Creates and owns a python venv and handles calls to it. Create using the
+ * {@link PythonPartyFactory}. This is preferred use, over PythonCall, because
+ * it runs your commands inside a venv. To use this the PYTHON3EXE environment
+ * variable must point to a working python3.exe version 3.8.5. The exe CAN be a
+ * symlink to the actual exe (at least, on mac it works fine)
+ */
+public class PythonVenv {
+	private final static String TMP = System.getProperty("java.io.tmpdir");
+	private final static String GENIUSWEBVENV = "GeniusWebPythonVenvs";
+	private final static String PYTHON3EXE = "PYTHON3EXE";
+	private final static String PYTHON3 = System.getenv(PYTHON3EXE);
+	private final Path venvpath;
+	private final Reporter logger;
+
+	private final static boolean IS_WINDOWS = System.getProperty("os.name")
+			.toLowerCase().contains("win");
+	private final static String VENV_PYTHON_EXE = IS_WINDOWS ? "Scripts\\python"
+			: "bin/python3";
+
+	/**
+	 * 
+	 * @param dirname a preferred dirname. Must be [a-zA-Z_0-9]+. You can remove
+	 *                non-word chars eg using dirname.replaceAll("\\W", "");
+	 * @param logger  the logger to report errors
+	 * @throws InterruptedException if the call does not terminate in time.
+	 * @throws IOException          if we can not connect properly to the python
+	 *                              process
+	 * @throws PythonError          if the python program printed to stderr, eg
+	 *                              when an error occured. We try to capture all
+	 *                              the error lines as text in the pythonerror
+	 *                              object.
+	 */
+	public PythonVenv(String dirname, Reporter logger)
+			throws IOException, InterruptedException, PythonError {
+		if (logger == null || dirname == null)
+			throw new IllegalArgumentException("args must be not null");
+		if (!(dirname.matches("\\w+")))
+			throw new IllegalArgumentException(
+					"dirname must be [a-zA-Z_0-9]+  but got " + dirname);
+		this.venvpath = Paths.get(TMP, GENIUSWEBVENV, dirname);
+		this.logger = logger;
+		createVenv();
+	}
+
+	/**
+	 * @see #call(List, int)
+	 * @param arg       this arg is just split according to whitespaces in the
+	 *                  string. Use {@link #call(List, int)} if your command
+	 *                  contains whitespaces.
+	 * @param timeoutms the timeout in ms
+	 * @return result of the call
+	 * @throws InterruptedException if the call does not terminate in time.
+	 * @throws IOException          if we can not connect properly to the python
+	 *                              process
+	 * @throws PythonError          if the python program printed to stderr, eg
+	 *                              when an error occured. We try to capture all
+	 *                              the error lines as text in the pythonerror
+	 *                              object.
+	 * @throws InterruptedException if call is interrupted
+	 */
+	public String call(String arg, int timeoutms)
+			throws InterruptedException, IOException, PythonError {
+		return call(Arrays.asList(arg.split(" ")), timeoutms);
+	}
+
+	/**
+	 * @param args      the arguments for the call, as simple string, We do
+	 *                  split on " " to get the arguments
+	 * @param timeoutms the timeout for the call in milliseconds.
+	 * @return the result of the call. The call will use the python3 and the
+	 *         settings in the venv. So you can safely dd eg "python -m pip
+	 *         install "
+	 * @throws InterruptedException if the call does not terminate in time.
+	 * @throws IOException          if we can not connect properly to the python
+	 *                              process
+	 * @throws PythonError          if the python program printed to stderr, eg
+	 *                              when an error occured. We try to capture all
+	 *                              the error lines as text in the pythonerror
+	 *                              object.
+	 * @throws InterruptedException if call is interrupted
+	 */
+	public String call(List<String> args, int timeoutms)
+			throws InterruptedException, IOException, PythonError {
+		return new Call(
+				venvpath.resolve(VENV_PYTHON_EXE).toAbsolutePath().toString(),
+				args, timeoutms).run();
+	}
+
+	/**
+	 * @param args      the call arguments (excluding the python startup
+	 *                  command)
+	 * @param timeoutms timeout for the call, actually is ignored currently
+	 * @return a {@link Pipe}, with given args and timeoutms
+	 */
+	public Pipe pipe(List<String> args, int timeoutms) {
+		return new Pipe(
+				venvpath.resolve(VENV_PYTHON_EXE).toAbsolutePath().toString(),
+				args, timeoutms, logger);
+	}
+
+	/**
+	 * This should be called when the pythonVenv is not used anymore. It will
+	 * not be deleted automatically because you may want to keep the venv for a
+	 * next run without needing to recreate it.
+	 */
+	public void remove() {
+		try {
+			FileUtils.deleteDirectory(venvpath.toFile());
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	private void createVenv()
+			throws IOException, InterruptedException, PythonError {
+		File venvdir = venvpath.toFile();
+		logger.log(Level.INFO, "creating venv at " + venvdir);
+		if (PYTHON3 == null)
+			throw new IllegalStateException("The environment variable "
+					+ PYTHON3EXE + " must be set to point to "
+					+ "the python3.8 or higher executable to be used");
+
+		String version = new Call(PYTHON3,
+				Arrays.asList("-c", "import sys; print(sys.version)"), 500)
+						.run();
+		if (!(version.startsWith("3.8") || version.startsWith("3.9"))) {
+			throw new IllegalStateException("Python version must be 3.8 or 3.9 "
+					+ " but " + PYTHON3EXE + " points to " + version);
+		}
+
+		if (venvdir.exists()) {
+			// #15 don't reuse, it may be corrupt or outdated.
+			FileUtils.deleteDirectory(venvdir);
+		}
+
+		if (!venvdir.mkdirs())
+			throw new IllegalStateException(
+					"Failed to create venv dir at " + venvdir);
+
+		new Call(PYTHON3,
+				Arrays.asList("-m", "venv", venvdir.getAbsolutePath()), 20000)
+						.run();
+
+	}
+
+}
