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
39 changes: 39 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Python CI

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set Up Rust Toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: '1.86.0'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
run: pip install uv
- name: Install dependencies
run: uv sync
- name: Install viceroy
run: cargo install --git https://github.com/fastly/Viceroy.git --branch sunfishcode/sync-wit viceroy
- name: Build WebAssembly component
run: make app.wasm
- name: Install dependencies
run: uv sync --extra dev --extra test
- name: Check formatting
run: make format-check
Comment thread
erikrose marked this conversation as resolved.
- name: Run linting
run: make lint
- name: Run tests
run: make test
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
*.egg-info
uv.lock

/hello_compute/
/stubs/
__pycache__
app.wasm
27 changes: 22 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
all: app.wasm

STUBS_DIR := stubs

app.wasm: wit/viceroy.wit wit/deps/fastly/compute.wit app.py
rm -rf hello_compute
uv run componentize-py -d wit -w compute bindings hello_compute
uv run componentize-py -d wit -w compute componentize --stub-wasi app -o app.wasm
rm -rf ${STUBS_DIR}
uv run componentize-py -d wit -w viceroy bindings ${STUBS_DIR}
uv run componentize-py -d wit -w viceroy componentize app -o app.wasm

serve: app.wasm
viceroy --adapt app.wasm
viceroy serve app.wasm

test: app.wasm
uv run --extra test pytest

lint:
uv run --extra dev ruff check .

lint-fix:
uv run --extra dev ruff check --fix .

format:
uv run --extra dev ruff format .

format-check:
uv run --extra dev ruff format --check .

.PHONY: all serve
.PHONY: all serve test lint format format-check
35 changes: 27 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ Currently, this demonstrates…
native-code compression stdlibs which haven't been compiled against WASI yet.
Moving componentize-py to a new Python may help, as [WASIp1 is now a Tier 2
supported platform](https://peps.python.org/pep-0011/#tier-2).
* It crashes every time something tries to write to stdout or stderr. It may be
that those aren't in the preopens; adding those to the preopens should be
possible with changes to Viceroy. We're also using `--stub-wasi` at the
moment, which means things like `fd_write` are coded to immediately trap; that
probably doesn't help. Finally, it may be possible to monkeypatch in Python
and redirect them to a logging endpoint, but our initial attempts were
unsuccessful.

# Install Dependencies

Expand All @@ -40,7 +33,33 @@ Currently, this demonstrates…
# Build and Run

1. `make serve`
2. Visit http://127.0.0.1:7676/hello/fred in a browser.
2. Visit http://127.0.0.1:7676/hello/world or http://127.0.0.1:7676/info in a browser.

You are seeing Bottle, a simple Python web framework, run on a Fastly Compute
worker!

# Testing

```bash
# Install dependencies and run tests
uv sync --extra dev --extra test
make test
```

The tests automatically build the WebAssembly component, start viceroy, and verify all endpoints work correctly with the WIT APIs.

# Development

```bash
# Format code
make format

# Check formatting
make format-check

# Run linting
make lint

# Run linting and apply automatic fixes
make lint-fix
```
104 changes: 36 additions & 68 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,49 @@
import sys
from urllib.parse import urlparse

import art
import bottle
from bottle import template, Bottle

from wit_world.exports import Reactor as BaseReactor
from wit_world.imports import log, http_req, http_resp, http_body
from bottle import Bottle
from wit_world.exports import HttpIncoming as BaseHttpIncoming
from wit_world.imports import compute_runtime, http_body, http_resp
from wit_world.imports.http_resp import send_downstream


# Enable a bit more debug logging from the framework.
bottle.debug(True)

app = Bottle()
app.catchall = False # bottle backtrace causes issues; use our own.


@app.route("/hello/<name>")
def index(name):
return template("Hello <pre>{{name}}</pre>", name=art.text2art(name))
def hello(name):
return f"Hello {name}!"


def print(*args):
# hack to allow print locally; so far, monkeypatching
# sys.stdout/sys.stderr hasn't panned out, so more
# research required.
log.write(log_ep, " ".join(args).encode())
@app.route("/info")
def info():
"""Return JSON with request information we can test against"""
from bottle import request

# Get some runtime info we can test
vcpu_time = compute_runtime.get_vcpu_ms()

def init():
global log_ep
log_ep = log.endpoint_get("")
return {
"service": "fastly-compute-python",
"status": "ok",
"message": "Hello from Fastly Compute!",
"vcpu_time_ms": vcpu_time,
"request_method": request.environ.get("REQUEST_METHOD"),
"path_info": request.environ.get("PATH_INFO"),
}

class StdErr:
"""File-like object to receive errors and direct them to our logging endpoint"""

def write(self, data: str):
print(f"wsgi-error: {data}")

def flush(self):
pass
@app.route("/error")
def error():
"""Endpoint that intentionally raises an exception to test error handling."""
raise RuntimeError("This is an intentional error for testing purposes")


def serve_wsgi_request(req, body, app):
"""Pass a WSGI application a single request, and adapt its behavior back
to the Fastly API."""

response = http_resp.new()
response = http_resp.Response.new()
response_body = http_body.new()

def write(body_data: bytes):
Expand All @@ -56,56 +53,27 @@ def write(body_data: bytes):

def start_response(status: str, headers: list[tuple], exc_info=None):
code, _description = status.split(" ", 1)
http_resp.status_set(response, int(code))
response.set_status(int(code))
Comment thread
erikrose marked this conversation as resolved.
for header, value in headers:
http_resp.header_append(response, header.encode(), value.encode())
response.append_header(header, value.encode())
return write

url = urlparse(http_req.uri_get(req, 2048))
url = urlparse(req.get_uri(2048))
environ = {
"REQUEST_METHOD": http_req.method_get(req, 12),
"REQUEST_METHOD": req.get_method(12),
"PATH_INFO": url.path,
"QUERY_STRING": url.query,
"SERVER_NAME": url.hostname,
"SERVER_PORT": str(url.port),
"wsgi.errors": StdErr(),
"wsgi.errors": sys.stderr,
}
for body_chunk in app(environ, start_response):
# TODO: this would be a good place to stream, but for now we just
# write to the buffer and send once the handler is done.
write(body_chunk)
send_downstream(response, response_body, False)


class Reactor(BaseReactor):
def serve(self, req: int, body: int) -> None:
init()
try:
serve_wsgi_request(req, body, app)
except Exception as e:
log_exception(e)


def log_exception(e):
"""Pretty-print an exception to our logging endpoint.

Do it without callling format_exc(), which calls stat() to determine whether
we're in a tty and what its width is. stat() and other fd routines currently
crash when they try to access stdout or stderr, probably because they are
not in the preopens.
"""
try:
print(f"Exception {type(e).__name__} - {e}")
print("--- Traceback Follows ---")

current_tb = e.__traceback__
while current_tb:
frame = current_tb.tb_frame
print(
f" File: {frame.f_code.co_filename}, "
f"Function: {frame.f_code.co_name}, "
f"Line: {frame.f_lineno}"
)
current_tb = current_tb.tb_next
except Exception as e2:
print(f"print_exc failed {e2}")
send_downstream(response, response_body)


class HttpIncoming(BaseHttpIncoming):
def handle(self, request, body):
serve_wsgi_request(request, body, app)
7 changes: 7 additions & 0 deletions fastly_compute/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Fastly Compute SDK for Python.

This package provides a Python SDK for building Fastly Compute services.
"""

# Testing utilities are available but not imported by default
# Users can import them explicitly: from fastly_compute.testing import ViceroyTestBase
31 changes: 31 additions & 0 deletions fastly_compute/pytest_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Pytest plugin for automatic viceroy output on test failures."""

import sys

import pytest


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Hook to display viceroy output on test failure.

This hook automatically displays recent viceroy server output
when any test fails, making debugging easier.
"""
outcome = yield
rep = outcome.get_result()

# Only show output on test failures during the call phase
if rep.when == "call" and rep.failed:
# Try to get the viceroy_server fixture from the test
if hasattr(item, "funcargs") and "viceroy_server" in item.funcargs:
server = item.funcargs["viceroy_server"]
if hasattr(server, "output_lines"):
print(
f"\n=== Viceroy output for failed test: {item.name} ===",
file=sys.stderr,
)
# Show last 15 lines of output
for line in server.output_lines[-15:]:
print(f" {line}", file=sys.stderr)
print("=== End viceroy output ===", file=sys.stderr)
Loading