This is an automated email from the ASF dual-hosted git repository.

rabbah pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-runtime-python.git


The following commit(s) were added to refs/heads/master by this push:
     new 14a31cf  Support python39 (#111)
14a31cf is described below

commit 14a31cfe22c2c7f4fd312110d11f80b1b40120c0
Author: Michele Sciabarra <[email protected]>
AuthorDate: Sat Jul 17 16:55:57 2021 +0200

    Support python39 (#111)
    
    * implementes python39, support for building venv with requirements.txt, 
updated tests
    
    * removed useless test, fixed a typo
    
    Co-authored-by: Michele Sciabarra <[email protected]>
---
 README.md                                          |  58 +++++++++--
 .../Dockerfile                                     |  10 +-
 .../README.md                                      |   0
 .../build.gradle                                   |  21 +++-
 .../requirements.txt                               |   0
 .../samples/smart-body-crop/.gitignore             |   0
 .../samples/smart-body-crop/common.py              |   6 +-
 .../samples/smart-body-crop/crop.ipynb             |   0
 .../samples/smart-body-crop/fashion-men-1.jpg      | Bin
 .../samples/smart-body-crop/inference.py           |   0
 .../Dockerfile                                     |  21 ++--
 .../build.gradle                                   |  21 +++-
 core/python39Action/requirements.txt               |  12 +++
 .../CHANGELOG.md                                   |   5 +
 .../Dockerfile                                     |  19 +---
 core/{python3ActionLoop => python3Action}/Makefile |   0
 .../bin/compile                                    |  50 +++++++--
 .../build.gradle                                   |   2 +-
 .../lib/launcher.py                                |   4 +-
 core/python3Action/requirements.txt                |  11 ++
 core/python3ActionLoop/lib/launcher.py             |  71 -------------
 core/python3AiActionLoop/Makefile                  |  32 ------
 core/python3AiActionLoop/bin/compile               | 113 ---------------------
 settings.gradle                                    |  14 ++-
 tests/build.gradle                                 |   6 ++
 tests/src/test/resources/build.sh                  |  22 ++--
 .../test/resources/python_virtualenv/__main__.py   |   9 +-
 .../src/test/resources/python_virtualenv/build.sh  |  22 ----
 .../resources/python_virtualenv/requirements.txt   |   2 +-
 .../python_virtualenv_invalid_main/build.sh        |  22 ----
 .../requirements.txt                               |  18 ----
 ...pContainerTests.scala => Python36AiTests.scala} |  10 +-
 ...oopContainerTests.scala => Python37Tests.scala} |  24 +++--
 .../runtime/actionContainers/Python39Tests.scala}  |  23 +++--
 ...pExtraTests.scala => PythonAdvancedTests.scala} |   4 +-
 ...ContainerTests.scala => PythonBasicTests.scala} |   2 +-
 tools/travis/publish.sh                            |   6 +-
 37 files changed, 249 insertions(+), 391 deletions(-)

diff --git a/README.md b/README.md
index b6c6a92..def82e3 100644
--- a/README.md
+++ b/README.md
@@ -47,10 +47,9 @@ Build using Python 3.7 (recommended)
 ```
 docker build -t actionloop-python-v3.7:1.0-SNAPSHOT 
$(pwd)/core/python3ActionLoop
 ```
-This tutorial assumes you're building with python 3.7. But if you want to use 
python 2.7 you can use:
-```
-docker build -t actionloop-python-v2.7:1.0-SNAPSHOT 
$(pwd)/core/python2ActionLoop
-```
+
+For runtime 3.9 or 3.6-ai you need also to copy `bin` and `lib` folders from 
3.7 in the Docker folder.
+
 
 2.1. Check docker `IMAGE ID` (3rd column) for repository 
`actionloop-python-v3.7`
 ```
@@ -344,10 +343,10 @@ To build all those images, run the following command.
 
 You can optionally build a specific image by modifying the Gradle command. For 
example:
 ```
-./gradlew core:python3ActionLoop:distDocker
+./gradlew core:python3Action:distDocker
 ```
 
-The build will produce Docker images such as `actionloop-python-v3.7`
+The build will produce Docker images such as `action-python-v3.7`
 and will also tag the same image with the `whisk/` prefix. The latter
 is a convenience, which if you're testing with a local OpenWhisk
 stack, allows you to skip pushing the image to Docker Hub.
@@ -374,11 +373,11 @@ in first with the `docker` CLI.
 ### Using Your Image as an OpenWhisk Action
 
 You can now use this image as an OpenWhisk action. For example, to use
-the image `actionloop-python-v3.7` as an action runtime, you would run
+the image `action-python-v3.7` as an action runtime, you would run
 the following command.
 
 ```
-wsk action update myAction myAction.py --docker 
$DOCKER_USER/actionloop-python-v3.7
+wsk action update myAction myAction.py --docker $DOCKER_USER/action-python-v3.7
 ```
 
 ## Test Runtimes
@@ -400,14 +399,53 @@ Gradle allows you to selectively run tests. For example, 
the following
 command runs tests which match the given pattern and excludes all
 others.
 ```
-./gradlew :tests:test --tests *ActionLoopContainerTests*
+./gradlew :tests:test --tests Python*Tests
 ```
 
 ## Python 3 AI Runtime
-This action runtime enables developers to create AI Services with OpenWhisk. 
It comes with preinstalled libraries useful for running machine learning and 
deep learning inferences. [Read more about this runtime 
here](./core/python3AiActionLoop).
+This action runtime enables developers to create AI Services with OpenWhisk. 
It comes with preinstalled libraries useful for running machine learning and 
deep learning inferences. [Read more about this runtime 
here](./core/python3AiAction).
 
 ## Import Project into IntelliJ
 
 Follow these steps to import the project into your IntelliJ IDE.
 - Import project as gradle project.
 - Make sure the working directory is root of the project/repo.
+
+# Using extra libraries
+
+If you need more libraries for your Python action,  you can include a 
virtualenv in the zip file of the action.
+
+The requirement is that the zip file must have a subfolder named `virtualenv` 
with a script `virtualenv\bin\activate_this.py` working in an Linux AMD64 
environment. It will be executed at start time to use your extra libraries.
+
+## Using requirements.txt
+
+Virtual envs are usually built listing your dependencies in a 
`requirements.txt`.
+
+If you have an action that requires addition libraries, you can just include 
`requirements.txt`.
+
+You have to create a folder `myaction` with at least two files:
+
+```
+__main__.py
+requirements.txt
+```
+
+Then zip your action and deploy to OpenWhisk, the requirements will be 
installed for you at init time, creating a suitable virtualenv.
+
+Keep in mind that resolving requirements involves downloading and install 
software, so your action timeout limit may need to be adjusted accordingly. 
Instead, you should consider using precompilation to resolve the requirements 
at build time.
+
+
+## Precompilation of a virtualenv
+
+The action containers can actually generate a virtualenv for you, provided you 
have a requirements.txt.
+
+
+If you have an action in the format described before (with a 
`requirements.txt`) you can build the zip file with the included files with:
+
+```
+zip -j -r myaction | docker run -i action-python-v3.7 -compile main 
>myaction.zip
+```
+
+You may use `v3.9` or `v3.6-ai` as well according to your Python version needs.
+
+The resulting action includes a virtualenv already built for you and that is 
fast to deploy and start as all the dependencies are already resolved. Note 
that there is a limit on the size of the zip file and this approach will not 
work for installing large libraries like Pandas or Numpy, instead use the 
provide "v.3.6-ai"  runtime instead which provides these libraries already for 
you.
diff --git a/core/python3AiActionLoop/Dockerfile 
b/core/python36AiAction/Dockerfile
similarity index 91%
rename from core/python3AiActionLoop/Dockerfile
rename to core/python36AiAction/Dockerfile
index b94237c..a6742fd 100644
--- a/core/python3AiActionLoop/Dockerfile
+++ b/core/python36AiAction/Dockerfile
@@ -37,7 +37,7 @@ RUN curl -sL \
 FROM tensorflow/tensorflow:1.15.2-py3-jupyter
 
 # select the builder to use
-ARG GO_PROXY_BUILD_FROM=release
+ARG GO_PROXY_BUILD_FROM=source
 
 RUN apt-get update && apt-get upgrade -y && apt-get install -y \
             curl \
@@ -53,7 +53,11 @@ RUN apt-get update && apt-get upgrade -y && apt-get install 
-y \
             && rm -rf /var/lib/apt/lists/*
 
 # PyTorch
-RUN pip3 install torch torchvision
+# persistent as it fails often
+RUN while ! pip list | grep torch ;\
+    do pip install torch ; done ;\
+    while ! pip list | grep torchvision ;\
+    do pip install torchvision ; done
 
 # rclone
 RUN curl -L https://downloads.rclone.org/rclone-current-linux-amd64.deb -o 
rclone.deb \
@@ -61,7 +65,7 @@ RUN curl -L 
https://downloads.rclone.org/rclone-current-linux-amd64.deb -o rclon
     && rm rclone.deb
 
 COPY requirements.txt requirements.txt
-RUN pip3 install --upgrade pip six &&\
+RUN pip3 install --upgrade pip six wheel &&\
     pip3 install --no-cache-dir -r requirements.txt &&\
     ln -sf /usr/bin/python3 /usr/local/bin/python
 
diff --git a/core/python3AiActionLoop/README.md 
b/core/python36AiAction/README.md
similarity index 100%
rename from core/python3AiActionLoop/README.md
rename to core/python36AiAction/README.md
diff --git a/core/python3ActionLoop/build.gradle 
b/core/python36AiAction/build.gradle
similarity index 69%
copy from core/python3ActionLoop/build.gradle
copy to core/python36AiAction/build.gradle
index 2e4226b..e915a75 100644
--- a/core/python3ActionLoop/build.gradle
+++ b/core/python36AiAction/build.gradle
@@ -15,5 +15,24 @@
  * limitations under the License.
  */
 
-ext.dockerImageName = 'actionloop-python-v3.7'
+ext.dockerImageName = 'action-python-v3.6-ai'
 apply from: '../../gradle/docker.gradle'
+
+distDocker.dependsOn 'copyLib'
+distDocker.dependsOn 'copyBin'
+distDocker.finalizedBy('cleanup')
+
+task copyLib(type: Copy) {
+    from '../python3Action/lib'
+    into './lib'
+}
+
+task copyBin(type: Copy) {
+    from '../python3Action/bin'
+    into './bin'
+}
+
+task cleanup(type: Delete) {
+    delete 'bin'
+    delete 'lib'
+}
diff --git a/core/python3AiActionLoop/requirements.txt 
b/core/python36AiAction/requirements.txt
similarity index 100%
rename from core/python3AiActionLoop/requirements.txt
rename to core/python36AiAction/requirements.txt
diff --git a/core/python3AiActionLoop/samples/smart-body-crop/.gitignore 
b/core/python36AiAction/samples/smart-body-crop/.gitignore
similarity index 100%
rename from core/python3AiActionLoop/samples/smart-body-crop/.gitignore
rename to core/python36AiAction/samples/smart-body-crop/.gitignore
diff --git a/core/python3AiActionLoop/samples/smart-body-crop/common.py 
b/core/python36AiAction/samples/smart-body-crop/common.py
similarity index 98%
rename from core/python3AiActionLoop/samples/smart-body-crop/common.py
rename to core/python36AiAction/samples/smart-body-crop/common.py
index 9aa16ac..4c6ece1 100644
--- a/core/python3AiActionLoop/samples/smart-body-crop/common.py
+++ b/core/python36AiAction/samples/smart-body-crop/common.py
@@ -84,7 +84,7 @@ CocoColors = [(255, 0, 0), (255, 85, 0), (255, 170, 0), (255, 
255, 0), (170, 255
 
 NMS_Threshold = 0.1
 InterMinAbove_Threshold = 6
-Inter_Threshold = 0.1
+Inter_Threashold = 0.1
 Min_Subset_Cnt = 4
 Min_Subset_Score = 0.8
 Max_Human = 96
@@ -162,7 +162,7 @@ def estimate_pose(heatMat, pafMat):
                 # if two humans share a part (same part idx and coordinates), 
merge those humans
                 if set(c1['uPartIdx']) & set(c2['uPartIdx']) != empty_set:
                     is_merged = True
-                    # extend human1 connections with human2 connections
+                    # extend human1 connectios with human2 connections
                     conns_by_human[h1].extend(conns_by_human[h2])
                     conns_by_human.pop(h2)  # delete human2
                     break
@@ -243,7 +243,7 @@ def get_score(x1, y1, x2, y2, pafMatX, pafMatY):
         pafYs[idx] = pafMatY[my][mx]
 
     local_scores = pafXs * vx + pafYs * vy
-    thidxs = local_scores > Inter_Threshold
+    thidxs = local_scores > Inter_Threashold
 
     return sum(local_scores * thidxs), sum(thidxs)
 
diff --git a/core/python3AiActionLoop/samples/smart-body-crop/crop.ipynb 
b/core/python36AiAction/samples/smart-body-crop/crop.ipynb
similarity index 100%
rename from core/python3AiActionLoop/samples/smart-body-crop/crop.ipynb
rename to core/python36AiAction/samples/smart-body-crop/crop.ipynb
diff --git a/core/python3AiActionLoop/samples/smart-body-crop/fashion-men-1.jpg 
b/core/python36AiAction/samples/smart-body-crop/fashion-men-1.jpg
similarity index 100%
rename from core/python3AiActionLoop/samples/smart-body-crop/fashion-men-1.jpg
rename to core/python36AiAction/samples/smart-body-crop/fashion-men-1.jpg
diff --git a/core/python3AiActionLoop/samples/smart-body-crop/inference.py 
b/core/python36AiAction/samples/smart-body-crop/inference.py
similarity index 100%
rename from core/python3AiActionLoop/samples/smart-body-crop/inference.py
rename to core/python36AiAction/samples/smart-body-crop/inference.py
diff --git a/core/python3ActionLoop/Dockerfile b/core/python39Action/Dockerfile
similarity index 84%
copy from core/python3ActionLoop/Dockerfile
copy to core/python39Action/Dockerfile
index da898f3..35ee88c 100644
--- a/core/python3ActionLoop/Dockerfile
+++ b/core/python39Action/Dockerfile
@@ -14,9 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+
 # build go proxy from source
 FROM golang:1.15 AS builder_source
-
 ARG GO_PROXY_GITHUB_USER=apache
 ARG GO_PROXY_GITHUB_BRANCH=master
 RUN git clone --branch ${GO_PROXY_GITHUB_BRANCH} \
@@ -33,23 +33,14 @@ RUN curl -sL \
   && cd openwhisk-runtime-go-*/main\
   && GO111MODULE=on go build -o /bin/proxy
 
-FROM python:3.7-buster
+FROM python:3.9-buster
 
 # select the builder to use
-ARG GO_PROXY_BUILD_FROM=release
+ARG GO_PROXY_BUILD_FROM=source
 
 # Install common modules for python
-RUN pip install \
-    beautifulsoup4==4.6.3 \
-    httplib2==0.11.3 \
-    kafka_python==1.4.3 \
-    lxml==4.2.5 \
-    python-dateutil==2.7.3 \
-    requests==2.19.1 \
-    scrapy==1.5.1 \
-    simplejson==3.16.0 \
-    virtualenv==16.0.0 \
-    twisted==18.7.0
+COPY requirements.txt requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
 
 RUN mkdir -p /action
 WORKDIR /
@@ -64,7 +55,7 @@ ENV OW_LOG_INIT_ERROR=1
 # the launcher must wait for an ack
 ENV OW_WAIT_FOR_ACK=1
 # using the runtime name to identify the execution environment
-ENV OW_EXECUTION_ENV=openwhisk/action-python-v3.7
+ENV OW_EXECUTION_ENV=openwhisk/action-python-v3.9
 # compiler script
 ENV OW_COMPILER=/bin/compile
 
diff --git a/core/python3AiActionLoop/build.gradle 
b/core/python39Action/build.gradle
similarity index 69%
rename from core/python3AiActionLoop/build.gradle
rename to core/python39Action/build.gradle
index ad8d697..9abe626 100644
--- a/core/python3AiActionLoop/build.gradle
+++ b/core/python39Action/build.gradle
@@ -15,5 +15,24 @@
  * limitations under the License.
  */
 
-ext.dockerImageName = 'actionloop-python-v3.6-ai'
+ext.dockerImageName = 'action-python-v3.9'
 apply from: '../../gradle/docker.gradle'
+
+distDocker.dependsOn 'copyLib'
+distDocker.dependsOn 'copyBin'
+distDocker.finalizedBy('cleanup')
+
+task copyLib(type: Copy) {
+    from '../python3Action/lib'
+    into './lib'
+}
+
+task copyBin(type: Copy) {
+    from '../python3Action/bin'
+    into './bin'
+}
+
+task cleanup(type: Delete) {
+    delete 'bin'
+    delete 'lib'
+}
diff --git a/core/python39Action/requirements.txt 
b/core/python39Action/requirements.txt
new file mode 100644
index 0000000..549a80f
--- /dev/null
+++ b/core/python39Action/requirements.txt
@@ -0,0 +1,12 @@
+# default available packages for action-python-v39
+beautifulsoup4 == 4.9.3
+httplib2 == 0.19.1
+kafka_python == 1.4.7
+lxml == 4.6.3
+python-dateutil == 2.8.1
+requests == 2.25.1
+scrapy == 1.8.0
+simplejson == 3.17.2
+virtualenv == 20.4.7
+twisted == 21.2.0
+netifaces == 0.11.0
diff --git a/core/python3ActionLoop/CHANGELOG.md 
b/core/python3Action/CHANGELOG.md
similarity index 85%
rename from core/python3ActionLoop/CHANGELOG.md
rename to core/python3Action/CHANGELOG.md
index 5ac5342..0dbefe9 100644
--- a/core/python3ActionLoop/CHANGELOG.md
+++ b/core/python3Action/CHANGELOG.md
@@ -19,6 +19,11 @@
 
 # Python 3 OpenWhisk Runtime Container
 
+# to include
+ - Use 1.17.1 of openwhisk-runtime-go to support symlinks in zips (required 
for virtualenvs)
+ - Support for python-3.9
+ - Support for generating actions with virtualenvs resolving requirements.txt
+
 ## 1.16.0
   - Introduce tutorial to deploy python runtimes locally (#101)
   - Use 1.17.0 release of openwhisk-runtime-go (#98)
diff --git a/core/python3ActionLoop/Dockerfile b/core/python3Action/Dockerfile
similarity index 85%
rename from core/python3ActionLoop/Dockerfile
rename to core/python3Action/Dockerfile
index da898f3..870cef6 100644
--- a/core/python3ActionLoop/Dockerfile
+++ b/core/python3Action/Dockerfile
@@ -14,9 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+
 # build go proxy from source
 FROM golang:1.15 AS builder_source
-
 ARG GO_PROXY_GITHUB_USER=apache
 ARG GO_PROXY_GITHUB_BRANCH=master
 RUN git clone --branch ${GO_PROXY_GITHUB_BRANCH} \
@@ -36,20 +36,11 @@ RUN curl -sL \
 FROM python:3.7-buster
 
 # select the builder to use
-ARG GO_PROXY_BUILD_FROM=release
+ARG GO_PROXY_BUILD_FROM=source
 
 # Install common modules for python
-RUN pip install \
-    beautifulsoup4==4.6.3 \
-    httplib2==0.11.3 \
-    kafka_python==1.4.3 \
-    lxml==4.2.5 \
-    python-dateutil==2.7.3 \
-    requests==2.19.1 \
-    scrapy==1.5.1 \
-    simplejson==3.16.0 \
-    virtualenv==16.0.0 \
-    twisted==18.7.0
+COPY requirements.txt requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
 
 RUN mkdir -p /action
 WORKDIR /
@@ -63,7 +54,7 @@ ADD lib/launcher.py /lib/launcher.py
 ENV OW_LOG_INIT_ERROR=1
 # the launcher must wait for an ack
 ENV OW_WAIT_FOR_ACK=1
-# using the runtime name to identify the execution environment
+# execution environment
 ENV OW_EXECUTION_ENV=openwhisk/action-python-v3.7
 # compiler script
 ENV OW_COMPILER=/bin/compile
diff --git a/core/python3ActionLoop/Makefile b/core/python3Action/Makefile
similarity index 100%
rename from core/python3ActionLoop/Makefile
rename to core/python3Action/Makefile
diff --git a/core/python3ActionLoop/bin/compile b/core/python3Action/bin/compile
similarity index 64%
rename from core/python3ActionLoop/bin/compile
rename to core/python3Action/bin/compile
index d8eba62..052f2c4 100755
--- a/core/python3ActionLoop/bin/compile
+++ b/core/python3Action/bin/compile
@@ -20,7 +20,7 @@
 
 from __future__ import print_function
 import os, os.path, sys, ast, shutil, subprocess, traceback
-import importlib
+import importlib, virtualenv
 from os.path import abspath, exists, dirname
 
 # write a file creating intermediate directories
@@ -54,6 +54,24 @@ def sources(launcher, main, src_dir):
           "from main__ import main as main",
           "from main__ import %s as main" % main )
 
+# build virtualenv if there is a requirements.txt
+def virtualenv(tgt_dir):
+    # check virtualenv
+    virtualenv_dir = abspath('%s/virtualenv' % tgt_dir)
+    requirements_txt = abspath("%s/requirements.txt" % tgt_dir)
+    if exists(requirements_txt):
+        if not os.path.isdir(virtualenv_dir):
+            cmd = "python -m virtualenv %s >/tmp/err 2>/tmp/err" % 
virtualenv_dir
+            if os.system(cmd) != 0:
+                with open("/tmp/err", "r") as f:
+                    sys.stderr.write(f.read())
+            else:
+                cmd = ". %s/bin/activate && python -m pip install -r %s 
>/tmp/err 2>/tmp/err" % (virtualenv_dir, requirements_txt)
+                if os.system(cmd) != 0:
+                    with open("/tmp/err", "r") as f:
+                        sys.stderr.write(f.read())
+    sys.stderr.flush()
+
 # compile sources
 def build(src_dir, tgt_dir):
     # in general, compile your program into an executable format
@@ -64,7 +82,7 @@ def build(src_dir, tgt_dir):
     tgt_file = "%s/exec" % tgt_dir
     write_file(tgt_file, """#!/bin/bash
 export PYTHONIOENCODING=UTF-8
-if [ "$(cat $0.env)" = "$__OW_EXECUTION_ENV" ]
+if [[ "$__OW_EXECUTION_ENV" == "" || "$(cat $0.env)" == "$__OW_EXECUTION_ENV" 
]]
 then cd "$(dirname $0)"
      exec /usr/local/bin/python exec__.py "$@"
 else echo "Execution Environment Mismatch"
@@ -73,25 +91,34 @@ else echo "Execution Environment Mismatch"
      exit 1
 fi
 """, True)
-    write_file("%s.env"%tgt_file, os.environ['__OW_EXECUTION_ENV'])
+    if os.environ.get("__OW_EXECUTION_ENV"):
+      write_file("%s.env"%tgt_file, os.environ['__OW_EXECUTION_ENV'])
     return tgt_file
 
 #check if a module exists
 def check(tgt_dir, module_name):
+    # activate virtualenv if any
+    path_to_virtualenv = abspath('%s/virtualenv' % tgt_dir)
+    if os.path.isdir(path_to_virtualenv):
+        activate_this_file = path_to_virtualenv + '/bin/activate_this.py'
+        if not os.path.exists(activate_this_file):
+            # check if this was packaged for windows
+            activate_this_file = path_to_virtualenv + 
'/Scripts/activate_this.py'
+        if os.path.exists(activate_this_file):
+            with open(activate_this_file) as f:
+                code = compile(f.read(), activate_this_file, 'exec')
+                exec(code, dict(__file__=activate_this_file))
+        else:
+            sys.stderr.write("Invalid virtualenv. Zip file does not include 
'activate_this.py'.\n")
+    # check module
     try:
         sys.path.append(tgt_dir)
         mod = importlib.util.find_spec(module_name)
         if mod:
             with open(mod.origin, "rb") as f:
                 ast.parse(f.read().decode("utf-8"))
-            # check virtualenv
-            path_to_virtualenv = abspath('%s/virtualenv' % tgt_dir)
-            if os.path.isdir(path_to_virtualenv):
-                activate_this_file = path_to_virtualenv + 
'/bin/activate_this.py'
-                if not os.path.exists(activate_this_file):
-                   sys.stderr.write('Invalid virtualenv. Zip file does not 
include activate_this.py')
         else:
-            sys.stderr.write("Zip file does not include %s" % module_name)
+            sys.stderr.write("Zip file does not include %s\n" % module_name)
     except SyntaxError as er:
         sys.stderr.write(er.msg)
     except Exception as ex:
@@ -107,7 +134,8 @@ if __name__ == '__main__':
     src_dir = abspath(sys.argv[2])
     tgt_dir = abspath(sys.argv[3])
     sources(launcher, sys.argv[1], src_dir)
-    tgt = build(abspath(sys.argv[2]), tgt_dir)
+    build(abspath(sys.argv[2]), tgt_dir)
+    virtualenv(tgt_dir)
     check(tgt_dir, "main__")
     sys.stdout.flush()
     sys.stderr.flush()
diff --git a/core/python3ActionLoop/build.gradle 
b/core/python3Action/build.gradle
similarity index 94%
rename from core/python3ActionLoop/build.gradle
rename to core/python3Action/build.gradle
index 2e4226b..527d982 100644
--- a/core/python3ActionLoop/build.gradle
+++ b/core/python3Action/build.gradle
@@ -15,5 +15,5 @@
  * limitations under the License.
  */
 
-ext.dockerImageName = 'actionloop-python-v3.7'
+ext.dockerImageName = 'action-python-v3.7'
 apply from: '../../gradle/docker.gradle'
diff --git a/core/python3AiActionLoop/lib/launcher.py 
b/core/python3Action/lib/launcher.py
similarity index 90%
rename from core/python3AiActionLoop/lib/launcher.py
rename to core/python3Action/lib/launcher.py
index 9773570..ccf0c68 100755
--- a/core/python3AiActionLoop/lib/launcher.py
+++ b/core/python3Action/lib/launcher.py
@@ -27,12 +27,14 @@ try:
   if os.path.isdir(path_to_virtualenv):
     # activate the virtualenv using activate_this.py contained in the 
virtualenv
     activate_this_file = path_to_virtualenv + '/bin/activate_this.py'
+    if not os.path.exists(activate_this_file): # try windows path
+      activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py'
     if os.path.exists(activate_this_file):
       with open(activate_this_file) as f:
         code = compile(f.read(), activate_this_file, 'exec')
         exec(code, dict(__file__=activate_this_file))
     else:
-      sys.stderr.write('Invalid virtualenv. Zip file does not include 
/virtualenv/bin/' + os.path.basename(activate_this_file) + '\n')
+      sys.stderr.write("Invalid virtualenv. Zip file does not include 
'activate_this.py'.\n")
       sys.exit(1)
 except Exception:
   traceback.print_exc(file=sys.stderr, limit=0)
diff --git a/core/python3Action/requirements.txt 
b/core/python3Action/requirements.txt
new file mode 100644
index 0000000..d60e88a
--- /dev/null
+++ b/core/python3Action/requirements.txt
@@ -0,0 +1,11 @@
+# default available packages for python3action
+beautifulsoup4 == 4.6.3
+httplib2 == 0.11.3
+kafka_python == 1.4.3
+lxml == 4.2.5
+python-dateutil == 2.7.3
+requests == 2.19.1
+scrapy == 1.5.1
+simplejson == 3.16.0
+virtualenv == 16.0.0
+twisted == 18.7.0
diff --git a/core/python3ActionLoop/lib/launcher.py 
b/core/python3ActionLoop/lib/launcher.py
deleted file mode 100755
index 9773570..0000000
--- a/core/python3ActionLoop/lib/launcher.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#
-# 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.
-#
-from __future__ import print_function
-from sys import stdin
-from sys import stdout
-from sys import stderr
-from os import fdopen
-import sys, os, json, traceback, warnings
-
-try:
-  # if the directory 'virtualenv' is extracted out of a zip file
-  path_to_virtualenv = os.path.abspath('./virtualenv')
-  if os.path.isdir(path_to_virtualenv):
-    # activate the virtualenv using activate_this.py contained in the 
virtualenv
-    activate_this_file = path_to_virtualenv + '/bin/activate_this.py'
-    if os.path.exists(activate_this_file):
-      with open(activate_this_file) as f:
-        code = compile(f.read(), activate_this_file, 'exec')
-        exec(code, dict(__file__=activate_this_file))
-    else:
-      sys.stderr.write('Invalid virtualenv. Zip file does not include 
/virtualenv/bin/' + os.path.basename(activate_this_file) + '\n')
-      sys.exit(1)
-except Exception:
-  traceback.print_exc(file=sys.stderr, limit=0)
-  sys.exit(1)
-
-# now import the action as process input/output
-from main__ import main as main
-
-out = fdopen(3, "wb")
-if os.getenv("__OW_WAIT_FOR_ACK", "") != "":
-    out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8'))
-    out.write(b'\n')
-    out.flush()
-
-env = os.environ
-while True:
-  line = stdin.readline()
-  if not line: break
-  args = json.loads(line)
-  payload = {}
-  for key in args:
-    if key == "value":
-      payload = args["value"]
-    else:
-      env["__OW_%s" % key.upper()]= args[key]
-  res = {}
-  try:
-    res = main(payload)
-  except Exception as ex:
-    print(traceback.format_exc(), file=stderr)
-    res = {"error": str(ex)}
-  out.write(json.dumps(res, ensure_ascii=False).encode('utf-8'))
-  out.write(b'\n')
-  stdout.flush()
-  stderr.flush()
-  out.flush()
diff --git a/core/python3AiActionLoop/Makefile 
b/core/python3AiActionLoop/Makefile
deleted file mode 100644
index 61560ee..0000000
--- a/core/python3AiActionLoop/Makefile
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# 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.
-#
-IMG=whisk/actionloop-python-v3.6-ai:latest
-
-build:
-       docker build -t $(IMG) -f Dockerfile .
-
-clean:
-       docker rmi -f $(IMG)
-
-debug:
-       docker run -p 8080:8080 \
-       -ti --entrypoint=/bin/bash -v $(PWD):/mnt \
-       -e OW_COMPILER=/mnt/bin/compile \
-       $(IMG)
-
-.PHONY: build clean  debug
-
diff --git a/core/python3AiActionLoop/bin/compile 
b/core/python3AiActionLoop/bin/compile
deleted file mode 100755
index d8eba62..0000000
--- a/core/python3AiActionLoop/bin/compile
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python3
-"""Python Action Builder
-#
-# 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.
-#
-"""
-
-from __future__ import print_function
-import os, os.path, sys, ast, shutil, subprocess, traceback
-import importlib
-from os.path import abspath, exists, dirname
-
-# write a file creating intermediate directories
-def write_file(file, body, executable=False):
-    try: os.makedirs(dirname(file), mode=0o755)
-    except: pass
-    with open(file, mode="wb") as f:
-        f.write(body.encode("utf-8"))
-    if executable:
-        os.chmod(file, 0o755)
-
-# copy a file eventually replacing a substring
-def copy_replace(src, dst, match=None, replacement=""):
-    with open(src, 'rb') as s:
-        body = s.read()
-        if match:
-            body = body.decode("utf-8").replace(match, replacement)
-        write_file(dst, body)
-
-# assemble sources
-def sources(launcher, main, src_dir):
-    # move exec in the right place if exists
-    src_file = "%s/exec" % src_dir
-    if exists(src_file):
-        os.rename(src_file, "%s/__main__.py" % src_dir)
-    if exists("%s/__main__.py" % src_dir):
-        os.rename("%s/__main__.py" % src_dir, "%s/main__.py" % src_dir)
-
-    # write the boilerplate in a temp dir
-    copy_replace(launcher, "%s/exec__.py" % src_dir,
-          "from main__ import main as main",
-          "from main__ import %s as main" % main )
-
-# compile sources
-def build(src_dir, tgt_dir):
-    # in general, compile your program into an executable format
-    # for scripting languages, move sources and create a launcher
-    # move away the action dir and replace with the new
-    shutil.rmtree(tgt_dir)
-    shutil.move(src_dir, tgt_dir)
-    tgt_file = "%s/exec" % tgt_dir
-    write_file(tgt_file, """#!/bin/bash
-export PYTHONIOENCODING=UTF-8
-if [ "$(cat $0.env)" = "$__OW_EXECUTION_ENV" ]
-then cd "$(dirname $0)"
-     exec /usr/local/bin/python exec__.py "$@"
-else echo "Execution Environment Mismatch"
-     echo "Expected: $(cat $0.env)"
-     echo "Actual: $__OW_EXECUTION_ENV"
-     exit 1
-fi
-""", True)
-    write_file("%s.env"%tgt_file, os.environ['__OW_EXECUTION_ENV'])
-    return tgt_file
-
-#check if a module exists
-def check(tgt_dir, module_name):
-    try:
-        sys.path.append(tgt_dir)
-        mod = importlib.util.find_spec(module_name)
-        if mod:
-            with open(mod.origin, "rb") as f:
-                ast.parse(f.read().decode("utf-8"))
-            # check virtualenv
-            path_to_virtualenv = abspath('%s/virtualenv' % tgt_dir)
-            if os.path.isdir(path_to_virtualenv):
-                activate_this_file = path_to_virtualenv + 
'/bin/activate_this.py'
-                if not os.path.exists(activate_this_file):
-                   sys.stderr.write('Invalid virtualenv. Zip file does not 
include activate_this.py')
-        else:
-            sys.stderr.write("Zip file does not include %s" % module_name)
-    except SyntaxError as er:
-        sys.stderr.write(er.msg)
-    except Exception as ex:
-        sys.stderr.write(ex)
-    sys.stderr.flush()
-
-if __name__ == '__main__':
-    if len(sys.argv) < 4:
-        sys.stdout.write("usage: <main-function> <source-dir> <target-dir>\n")
-        sys.stdout.flush()
-        sys.exit(1)
-    launcher = "%s/lib/launcher.py" % dirname(dirname(sys.argv[0]))
-    src_dir = abspath(sys.argv[2])
-    tgt_dir = abspath(sys.argv[3])
-    sources(launcher, sys.argv[1], src_dir)
-    tgt = build(abspath(sys.argv[2]), tgt_dir)
-    check(tgt_dir, "main__")
-    sys.stdout.flush()
-    sys.stderr.flush()
diff --git a/settings.gradle b/settings.gradle
index d02dad7..82b9a9a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -17,8 +17,9 @@
 
 include 'tests'
 
-include 'core:python3ActionLoop'
-include 'core:python3AiActionLoop'
+include 'core:python3Action'
+include 'core:python36AiAction'
+include 'core:python39Action'
 
 rootProject.name = 'runtime-python'
 
@@ -27,11 +28,18 @@ gradle.ext.openwhisk = [
 ]
 
 gradle.ext.scala = [
-    version: '2.12.7',
+    version: '2.12.10',
+    depVersion  : '2.12',
     compileFlags: ['-feature', '-unchecked', '-deprecation', 
'-Xfatal-warnings', '-Ywarn-unused-import']
 ]
 
+gradle.ext.akka = [version : '2.6.12']
+gradle.ext.akka_http = [version : '10.2.4']
+
 gradle.ext.scalafmt = [
     version: '1.5.0',
     config: new File(rootProject.projectDir, '.scalafmt.conf')
 ]
+
+gradle.ext.akka = [version: '2.6.12']
+gradle.ext.akka_http = [version: '10.2.4']
diff --git a/tests/build.gradle b/tests/build.gradle
index e720728..0454521 100644
--- a/tests/build.gradle
+++ b/tests/build.gradle
@@ -37,6 +37,12 @@ dependencies {
     compile "org.scala-lang:scala-library:${gradle.scala.version}"
     compile 
"org.apache.openwhisk:openwhisk-tests:${gradle.openwhisk.version}:tests"
     compile 
"org.apache.openwhisk:openwhisk-tests:${gradle.openwhisk.version}:test-sources"
+    implementation group: 'com.typesafe.akka', name: 
"akka-http2-support_${gradle.scala.depVersion}", version: 
"${gradle.akka_http.version}"
+    implementation group: 'com.typesafe.akka', name: 
"akka-http-xml_${gradle.scala.depVersion}", version: 
"${gradle.akka_http.version}"
+    implementation group: 'com.typesafe.akka', name: 
"akka-discovery_${gradle.scala.depVersion}", version: "${gradle.akka.version}"
+    implementation group: 'com.typesafe.akka', name: 
"akka-protobuf_${gradle.scala.depVersion}", version: "${gradle.akka.version}"
+    implementation group: 'com.typesafe.akka', name: 
"akka-remote_${gradle.scala.depVersion}", version: "${gradle.akka.version}"
+    implementation group: 'com.typesafe.akka', name: 
"akka-cluster_${gradle.scala.depVersion}", version: "${gradle.akka.version}"
 }
 
 tasks.withType(ScalaCompile) {
diff --git a/tests/src/test/resources/build.sh 
b/tests/src/test/resources/build.sh
index e65166f..bbc4616 100755
--- a/tests/src/test/resources/build.sh
+++ b/tests/src/test/resources/build.sh
@@ -16,25 +16,17 @@
 # limitations under the License.
 #
 
-set -e
-
 if [ -f ".built" ]; then
   echo "Test zip artifacts already built, skipping"
   exit 0
 fi
 
-# see what version of python is running
-py=$(python --version 2>&1 | awk -F' ' '{print $2}')
-if [[ $py == 3.7.* ]]; then
-  echo "python version is $py (ok)"
-else
-  echo "python version is $py (not ok)"
-  echo "cannot generated test artifacts and tests will fail"
-  exit -1
-fi
-
-(cd python_virtualenv && ./build.sh && zip ../python_virtualenv.zip -r .)
-(cd python_virtualenv_invalid_main && ./build.sh && zip 
../python_virtualenv_invalid_main.zip -r .)
-(cd python_virtualenv_invalid_venv && zip 
../python_virtualenv_invalid_venv.zip -r .)
+for i in v3.7 v3.6-ai v3.9
+do echo "*** $i ***"
+   zip -r -j - python_virtualenv | docker run -i action-python-$i -compile 
main >python-${i}_virtualenv.zip
+   cp python-${i}_virtualenv.zip python-${i}_virtualenv_invalid_main.zip
+   zip -d python-${i}_virtualenv_invalid_main.zip main__.py
+   cd python_virtualenv_invalid_venv/ ; zip 
../python-${i}_virtualenv_invalid_venv.zip * ; cd ..
+done
 
 touch .built
diff --git a/tests/src/test/resources/python_virtualenv/__main__.py 
b/tests/src/test/resources/python_virtualenv/__main__.py
index b0f4087..04b59af 100755
--- a/tests/src/test/resources/python_virtualenv/__main__.py
+++ b/tests/src/test/resources/python_virtualenv/__main__.py
@@ -18,11 +18,14 @@
  * limitations under the License.
  */
 """
-
-from random_useragent.random_useragent import Randomize
+from random_user_agent.user_agent import UserAgent
+from random_user_agent.params import HardwareType, OperatingSystem
 
 def main(args):
-    return {"agent": Randomize().random_agent('desktop','linux')}
+    user_agent_rotator = UserAgent(limit=100,
+        hardware_types=[HardwareType.COMPUTER.value],
+        operating_systems=[OperatingSystem.LINUX.value])
+    return {"agent": user_agent_rotator.get_random_user_agent()}
 
 def naim(args):
     return main(args)
diff --git a/tests/src/test/resources/python_virtualenv/build.sh 
b/tests/src/test/resources/python_virtualenv/build.sh
deleted file mode 100755
index dbf5aa0..0000000
--- a/tests/src/test/resources/python_virtualenv/build.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-#
-# 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.
-#
-
-virtualenv virtualenv
-source virtualenv/bin/activate
-pip install -r requirements.txt
-deactivate
diff --git a/tests/src/test/resources/python_virtualenv/requirements.txt 
b/tests/src/test/resources/python_virtualenv/requirements.txt
index 9dcd8c5..d2af60b 100644
--- a/tests/src/test/resources/python_virtualenv/requirements.txt
+++ b/tests/src/test/resources/python_virtualenv/requirements.txt
@@ -15,4 +15,4 @@
 # limitations under the License.
 #
 
-random-useragent==1.0
+random-user-agent==1.0.1
diff --git a/tests/src/test/resources/python_virtualenv_invalid_main/build.sh 
b/tests/src/test/resources/python_virtualenv_invalid_main/build.sh
deleted file mode 100755
index dbf5aa0..0000000
--- a/tests/src/test/resources/python_virtualenv_invalid_main/build.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-#
-# 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.
-#
-
-virtualenv virtualenv
-source virtualenv/bin/activate
-pip install -r requirements.txt
-deactivate
diff --git 
a/tests/src/test/resources/python_virtualenv_invalid_main/requirements.txt 
b/tests/src/test/resources/python_virtualenv_invalid_main/requirements.txt
deleted file mode 100644
index 9dcd8c5..0000000
--- a/tests/src/test/resources/python_virtualenv_invalid_main/requirements.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# 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.
-#
-
-random-useragent==1.0
diff --git 
a/tests/src/test/scala/runtime/actionContainers/Python3AiActionLoopContainerTests.scala
 b/tests/src/test/scala/runtime/actionContainers/Python36AiTests.scala
similarity index 95%
rename from 
tests/src/test/scala/runtime/actionContainers/Python3AiActionLoopContainerTests.scala
rename to tests/src/test/scala/runtime/actionContainers/Python36AiTests.scala
index 456f3b7..931489f 100644
--- 
a/tests/src/test/scala/runtime/actionContainers/Python3AiActionLoopContainerTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/Python36AiTests.scala
@@ -19,17 +19,15 @@ package runtime.actionContainers
 
 import org.junit.runner.RunWith
 import org.scalatest.junit.JUnitRunner
-import common.WskActorSystem
 import spray.json._
 import DefaultJsonProtocol._
 
 @RunWith(classOf[JUnitRunner])
-class Python3AiActionLoopContainerTests
-    extends PythonActionContainerTests
-    with PythonActionLoopExtraTests
-    with WskActorSystem {
+class Python36AiTests extends Python37Tests {
 
-  override lazy val imageName = "actionloop-python-v3.6-ai"
+  override lazy val imageName = "action-python-v3.6-ai"
+
+  override lazy val zipPrefix = "python-v3.6-ai"
 
   override lazy val errorCodeOnRun = false
 
diff --git 
a/tests/src/test/scala/runtime/actionContainers/PythonActionLoopContainerTests.scala
 b/tests/src/test/scala/runtime/actionContainers/Python37Tests.scala
similarity index 82%
rename from 
tests/src/test/scala/runtime/actionContainers/PythonActionLoopContainerTests.scala
rename to tests/src/test/scala/runtime/actionContainers/Python37Tests.scala
index f1e9f82..be40663 100644
--- 
a/tests/src/test/scala/runtime/actionContainers/PythonActionLoopContainerTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/Python37Tests.scala
@@ -26,12 +26,11 @@ import org.scalatest.junit.JUnitRunner
 import spray.json._
 
 @RunWith(classOf[JUnitRunner])
-class PythonActionLoopContainerTests
-    extends PythonActionContainerTests
-    with PythonActionLoopExtraTests
-    with WskActorSystem {
+class Python37Tests extends PythonBasicTests with PythonAdvancedTests with 
WskActorSystem {
 
-  override lazy val imageName = "actionloop-python-v3.7"
+  override lazy val imageName = "action-python-v3.7"
+
+  lazy val zipPrefix = "python-v3.7"
 
   override val testNoSource = TestConfig("", hasCodeStub = false)
 
@@ -43,7 +42,7 @@ class PythonActionLoopContainerTests
   }
 
   it should "run zipped Python action containing a virtual environment" in {
-    val zippedPythonAction = testArtifact("python_virtualenv.zip")
+    val zippedPythonAction = testArtifact(zipPrefix + "_virtualenv.zip")
     val code = readAsBase64(zippedPythonAction.toPath)
 
     withActionContainer() { c =>
@@ -57,7 +56,7 @@ class PythonActionLoopContainerTests
   }
 
   it should "run zipped Python action containing a virtual environment with 
non-standard entry point" in {
-    val zippedPythonAction = testArtifact("python_virtualenv.zip")
+    val zippedPythonAction = testArtifact(zipPrefix + "_virtualenv.zip")
     val code = readAsBase64(zippedPythonAction.toPath)
 
     withActionContainer() { c =>
@@ -71,7 +70,7 @@ class PythonActionLoopContainerTests
   }
 
   it should "report error if zipped Python action has wrong main module name" 
in {
-    val zippedPythonAction = testArtifact("python_virtualenv_invalid_main.zip")
+    val zippedPythonAction = testArtifact(zipPrefix + 
"_virtualenv_invalid_main.zip")
     val code = readAsBase64(zippedPythonAction.toPath)
 
     val (out, err) = withActionContainer() { c =>
@@ -79,19 +78,22 @@ class PythonActionLoopContainerTests
       initCode should be(502)
 
       if (!errorCodeOnRun)
-        initRes.get.fields.get("error").get.toString should include regex ("No 
module|action failed")
+        initRes.get.fields
+          .get("error")
+          .get
+          .toString should include regex ("Cannot start action. Check logs for 
details.")
     }
 
     if (errorCodeOnRun)
       checkStreams(out, err, {
         case (o, e) =>
           o shouldBe empty
-          e should include("Zip file does not include __main__.py")
+          e should include("Zip file does not include")
       })
   }
 
   it should "report error if zipped Python action has invalid virtualenv 
directory" in {
-    val zippedPythonAction = testArtifact("python_virtualenv_invalid_venv.zip")
+    val zippedPythonAction = testArtifact(zipPrefix + 
"_virtualenv_invalid_venv.zip")
     val code = readAsBase64(zippedPythonAction.toPath)
 
     val (out, err) = withActionContainer() { c =>
diff --git a/tests/src/test/resources/python_virtualenv_invalid_main/mymain.py 
b/tests/src/test/scala/runtime/actionContainers/Python39Tests.scala
old mode 100755
new mode 100644
similarity index 67%
rename from tests/src/test/resources/python_virtualenv_invalid_main/mymain.py
rename to tests/src/test/scala/runtime/actionContainers/Python39Tests.scala
index b0f4087..344e672
--- a/tests/src/test/resources/python_virtualenv_invalid_main/mymain.py
+++ b/tests/src/test/scala/runtime/actionContainers/Python39Tests.scala
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-"""Python Hello virtualenv test.
-
 /*
  * Licensed to the Apache Software Foundation (ASF) under one or more
  * contributor license agreements.  See the NOTICE file distributed with
@@ -17,12 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-"""
 
-from random_useragent.random_useragent import Randomize
+package runtime.actionContainers
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class Python39Tests extends Python37Tests {
+
+  override lazy val imageName = "action-python-v3.9"
+
+  override lazy val zipPrefix = "python-v3.9"
 
-def main(args):
-    return {"agent": Randomize().random_agent('desktop','linux')}
+  override lazy val errorCodeOnRun = false
 
-def naim(args):
-    return main(args)
+  override val testNoSource = TestConfig("", hasCodeStub = false)
+}
diff --git 
a/tests/src/test/scala/runtime/actionContainers/PythonActionLoopExtraTests.scala
 b/tests/src/test/scala/runtime/actionContainers/PythonAdvancedTests.scala
similarity index 97%
rename from 
tests/src/test/scala/runtime/actionContainers/PythonActionLoopExtraTests.scala
rename to 
tests/src/test/scala/runtime/actionContainers/PythonAdvancedTests.scala
index 6a75c0a..d4bd9d9 100644
--- 
a/tests/src/test/scala/runtime/actionContainers/PythonActionLoopExtraTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/PythonAdvancedTests.scala
@@ -18,8 +18,8 @@ package runtime.actionContainers
 
 import spray.json.{JsObject, JsString}
 
-trait PythonActionLoopExtraTests {
-  this: PythonActionContainerTests =>
+trait PythonAdvancedTests {
+  this: PythonBasicTests =>
 
   it should "detect termination at run" in {
     val (out, err) = withActionContainer() { c =>
diff --git 
a/tests/src/test/scala/runtime/actionContainers/PythonActionContainerTests.scala
 b/tests/src/test/scala/runtime/actionContainers/PythonBasicTests.scala
similarity index 98%
rename from 
tests/src/test/scala/runtime/actionContainers/PythonActionContainerTests.scala
rename to tests/src/test/scala/runtime/actionContainers/PythonBasicTests.scala
index be810d1..3e06160 100644
--- 
a/tests/src/test/scala/runtime/actionContainers/PythonActionContainerTests.scala
+++ b/tests/src/test/scala/runtime/actionContainers/PythonBasicTests.scala
@@ -24,7 +24,7 @@ import actionContainers.{ActionContainer, 
BasicActionRunnerTests}
 import actionContainers.ActionContainer.withContainer
 import actionContainers.ResourceHelpers.ZipBuilder
 
-abstract class PythonActionContainerTests extends BasicActionRunnerTests with 
WskActorSystem {
+abstract class PythonBasicTests extends BasicActionRunnerTests with 
WskActorSystem {
 
   val imageName: String
 
diff --git a/tools/travis/publish.sh b/tools/travis/publish.sh
index c93c537..015e953 100755
--- a/tools/travis/publish.sh
+++ b/tools/travis/publish.sh
@@ -31,9 +31,11 @@ RUNTIME_VERSION=$2
 IMAGE_TAG=$3
 
 if [ ${RUNTIME_VERSION} == "3" ]; then
-  RUNTIME="python3ActionLoop"
+  RUNTIME="python3Action"
 elif [ ${RUNTIME_VERSION} == "3-ai" ]; then
-  RUNTIME="python3AiActionLoop"
+  RUNTIME="python3AiAction"
+elif [ ${RUNTIME_VERSION} == "39" ]; then
+  RUNTIME="python39Action"
 fi
 
 if [[ ! -z ${DOCKER_USER} ]] && [[ ! -z ${DOCKER_PASSWORD} ]]; then

Reply via email to