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 @@ jobs: 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: \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 @@ -126,6 +126,20 @@ "attachmentType": "text/plain" } }, + { + "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,