Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions runtime/python/v3.13/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
140 changes: 140 additions & 0 deletions runtime/python/v3.13/bin/compile
Original file line number Diff line number Diff line change
@@ -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()
73 changes: 73 additions & 0 deletions runtime/python/v3.13/lib/launcher.py
Original file line number Diff line number Diff line change
@@ -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()
42 changes: 42 additions & 0 deletions runtime/python/v3.13/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions runtimes.json.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading