feat: python v3.13
added python v3.13 with uv. Tried to optimize the Image size.
diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml
index d25374c..96e8cab 100644
--- a/.github/workflows/image.yaml
+++ b/.github/workflows/image.yaml
@@ -101,7 +101,7 @@
strategy:
max-parallel: 1
matrix:
- version: [v3.10,v3.11,v3.12]
+ version: [v3.10,v3.11,v3.12,v3.13]
steps:
- name: Checkout recursive
uses: actions/checkout@v2
diff --git a/runtime/python/v3.13/Dockerfile b/runtime/python/v3.13/Dockerfile
new file mode 100644
index 0000000..25a915c
--- /dev/null
+++ b/runtime/python/v3.13/Dockerfile
@@ -0,0 +1,70 @@
+#
+# 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.
+#
+
+ARG COMMON=missing:missing
+FROM ${COMMON} AS builder
+
+FROM python:3.13.4-slim-bookworm AS build-env
+
+# Set environment for uv installation
+ENV UV_CACHE_DIR=/tmp/uv-cache \
+ UV_INSTALL_DIR=/usr/local/bin
+
+# Install build tools and install uv
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ curl ca-certificates build-essential python3-dev && \
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
+ apt-get purge -y curl && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install Python dependencies
+WORKDIR /build
+COPY requirements.txt .
+RUN uv pip install --python python3 --system six wheel virtualenv
+RUN uv pip install --python python3 --system --no-cache-dir -r requirements.txt
+
+# Final minimal runtime
+FROM python:3.13.4-slim-bookworm
+
+# Set runtime environment
+ENV OW_EXECUTION_ENV=apacheopenserverless/runtime-python-v3.13.4 \
+ HOME=/tmp \
+ OW_LOG_INIT_ERROR=1 \
+ OW_WAIT_FOR_ACK=1 \
+ OW_COMPILER=/bin/compile
+
+# Install only runtime deps
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ python3-psycopg2 zip xpdf ca-certificates && \
+ apt-get autoremove -y && \
+ rm -rf /var/lib/apt/lists/*
+
+# Copy uv binary and Python packages from builder
+COPY --from=build-env /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/
+COPY --from=build-env /usr/local/lib/python3.13 /usr/local/lib/python3.13
+
+# Copy OpenWhisk runtime and proxy binary
+COPY --from=builder /go/bin/proxy /bin/proxy
+ADD bin/compile /bin/compile
+ADD lib/launcher.py /lib/launcher.py
+
+# Prepare /action
+WORKDIR /action
+RUN chown nobody:root /action && chmod 0775 /action
+
+USER nobody
+ENTRYPOINT ["/bin/proxy"]
diff --git a/runtime/python/v3.13/bin/compile b/runtime/python/v3.13/bin/compile
new file mode 100755
index 0000000..a85d56e
--- /dev/null
+++ b/runtime/python/v3.13/bin/compile
@@ -0,0 +1,140 @@
+#!/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, virtualenv
+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 )
+
+# 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
+ # 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 [[ "$__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"
+ echo "Expected: $(cat $0.env)"
+ echo "Actual: $__OW_EXECUTION_ENV"
+ exit 1
+fi
+""", True)
+ 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"))
+ else:
+ 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:
+ 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)
+ build(abspath(sys.argv[2]), tgt_dir)
+ check(tgt_dir, "main__")
+ sys.stdout.flush()
+ sys.stderr.flush()
diff --git a/runtime/python/v3.13/lib/launcher.py b/runtime/python/v3.13/lib/launcher.py
new file mode 100755
index 0000000..ccf0c68
--- /dev/null
+++ b/runtime/python/v3.13/lib/launcher.py
@@ -0,0 +1,73 @@
+#
+# 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 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 'activate_this.py'.\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/runtime/python/v3.13/requirements.txt b/runtime/python/v3.13/requirements.txt
new file mode 100644
index 0000000..1b276e8
--- /dev/null
+++ b/runtime/python/v3.13/requirements.txt
@@ -0,0 +1,42 @@
+beautifulsoup4==4.13.4
+ollama==0.4.5
+openai==1.59.3
+pymilvus==2.5.3
+redis==5.2.1
+pillow==11.1.0
+nltk==3.8.1
+httplib2==0.19.1
+kafka_python==2.0.2
+python-dateutil==2.8.2
+requests==2.32.2
+scrapy==2.5.0
+simplejson==3.17.5
+twisted==21.7.0
+netifaces==0.11.0
+pyyaml==6.0.2
+boto3==1.35.98
+psycopg==3.1.10
+pymongo==4.4.1
+minio==7.1.16
+auth0-python==4.6.0
+langdetect==1.0.9
+plotly==5.19.0
+joblib==1.4.2
+lightgbm==4.5.0
+feedparser==6.0.11
+numpy==1.26.4
+scikit-learn==1.5.2
+langchain==0.3.14
+langchain-ollama==0.2.2
+langchain-openai==0.2.14
+langchain-anthropic==0.3.1
+langchain-together==0.2.0
+langchain-postgres==0.0.12
+langchain-milvus==0.1.7
+bcrypt==4.2.1
+chevron==0.14.0
+chess==1.11.1
+uvicorn==0.34.2
+fastapi==0.115.12
+starlette==0.46.2
+mcp==1.6.0
\ No newline at end of file
diff --git a/runtimes.json.tpl b/runtimes.json.tpl
index 88ee1db..cf13a84 100644
--- a/runtimes.json.tpl
+++ b/runtimes.json.tpl
@@ -127,6 +127,20 @@
}
},
{
+ "kind": "python:3.13",
+ "default": false,
+ "image": {
+ "prefix": "$OPS_RUNTIME_PREFIX",
+ "name": "openserverless-runtime-python",
+ "tag": "$OPS_RUNTIME_TAG_PYTHON_V3_13"
+ },
+ "deprecated": false,
+ "attached": {
+ "attachmentName": "codefile",
+ "attachmentType": "text/plain"
+ }
+ },
+ {
"kind": "python:3.11ca",
"default": false,
"image": {