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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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