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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ all: $(COMPOSED_WASMS)

$(STUBS_DIR): $(COMPUTE_WIT)
rm -rf $(STUBS_DIR)
uv run --extra dev componentize-py -d wit --world-module wit_world -w $(TARGET_WORLD) bindings $(STUBS_DIR)
$(FASTLY_COMPUTE_PY) bindings -d wit -w $(TARGET_WORLD) --world-module wit_world $(STUBS_DIR)

# Build our composed wasm using fastly-compute-py build
$(BUILD_DIR)/%.composed.wasm: wit/viceroy.wit wit/deps/fastly/compute.wit fastly_compute/wsgi.py fastly_compute/runtime_patching/patches.py | $(BUILD_DIR) $(STUBS_DIR)
Expand Down
18 changes: 18 additions & 0 deletions crates/fastly-compute-py/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,22 @@ pub enum Command {
#[arg(short, long)]
virtualenv: Option<PathBuf>,
},

/// Generate WIT binding stubs for use with type checkers and IDEs
Bindings {
/// WIT directory to generate bindings from (default: wit)
#[arg(short = 'd', long)]
wit_dir: Option<PathBuf>,

/// WIT world to target (e.g. fastly:compute/service@0.1.0)
#[arg(short = 'w', long)]
world: Option<String>,

/// Python module name for the generated bindings (default: wit_world)
#[arg(long)]
world_module: Option<String>,

/// Output directory for the generated stubs
output_dir: PathBuf,
},
}
1 change: 1 addition & 0 deletions crates/fastly-compute-py/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ impl ConfigBuilder {
self.cli.output = output.clone();
self.cli.virtualenv = virtualenv.clone();
}
Command::Bindings { .. } => {}
}
self
}
Expand Down
79 changes: 66 additions & 13 deletions crates/fastly-compute-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,40 @@ fn init_logging(verbose: u8) {
pub fn run_main(cli: &Cli) -> Result<()> {
init_logging(cli.verbose);

log::info!("Building Python application for Fastly Compute...");
match &cli.command {
cli::Command::Build { .. } => {
log::info!("Building Python application for Fastly Compute...");

let config = ConfigBuilder::from_pyproject()
.unwrap_or_else(|e| {
log::warn!("Failed to load pyproject.toml: {}", e);
ConfigBuilder::default()
})
.with_command(&cli.command)
.resolve();
let config = ConfigBuilder::from_pyproject()
.unwrap_or_else(|e| {
log::warn!("Failed to load pyproject.toml: {}", e);
ConfigBuilder::default()
})
.with_command(&cli.command)
.resolve();

log::debug!("Final resolved configuration: {config:?}");
log::info!(" Entry point: {}", config.entry);
log::info!(" Output: {}", config.output.display());
log::debug!("Final resolved configuration: {config:?}");
log::info!(" Entry point: {}", config.entry);
log::info!(" Output: {}", config.output.display());

build(config.output.clone(), config.entry, config.virtualenv)?;
build(config.output.clone(), config.entry, config.virtualenv)?;

log::info!("✓ Build complete: {}", config.output.display());
log::info!("✓ Build complete: {}", config.output.display());
}
cli::Command::Bindings {
wit_dir,
world,
world_module,
output_dir,
} => {
generate_bindings(
wit_dir.as_deref(),
world.as_deref(),
world_module.as_deref(),
output_dir,
)?;
}
}

Ok(())
}
Expand All @@ -119,6 +136,42 @@ fn _fastly_compute_py(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
Ok(())
}

pub fn generate_bindings(
wit_dir: Option<&Path>,
world: Option<&str>,
world_module: Option<&str>,
output_dir: &Path,
) -> Result<()> {
let wit_path = wit_dir.unwrap_or(Path::new("wit"));
let worlds: &[&str] = match world {
Some(w) => &[w],
None => &[],
};

log::info!("Generating WIT bindings...");
log::debug!(" WIT path: {}", wit_path.display());
log::debug!(" World: {:?}", world);
log::debug!(" World module: {:?}", world_module);
log::debug!(" Output dir: {}", output_dir.display());

componentize_py::BindingsGenerator {
wit_path: &[wit_path],
worlds,
features: &[],
all_features: false,
world_module,
output_dir,
import_interface_names: &HashMap::new(),
export_interface_names: &HashMap::new(),
full_names: false,
}
.generate()?;

log::info!("✓ Bindings generated: {}", output_dir.display());

Ok(())
}

pub fn build(output: PathBuf, entry_name: String, virtualenv: Option<PathBuf>) -> Result<()> {
let temp_dir = TempDir::new()?;
let temp_path = temp_dir.path();
Expand Down
1 change: 0 additions & 1 deletion examples/backend-requests/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion examples/bottle-app/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion examples/flask-app/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion examples/game-of-life/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions fastly_compute/testing/stubs/poll_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

This also includes helper classes and functions for working with `wasi:http`.

As of WASI Preview 2, there is not yet a standard for first-class, composable
asynchronous functions and streams. We expect that little or none of this
boilerplate will be needed once those features arrive in Preview 3.
This is only useful for `wasi:http@0.2.x`; `wasi:http@0.3.x` uses a different
mechanism to model concurrency.
"""

import asyncio
Expand Down
4 changes: 2 additions & 2 deletions fastly_compute/testing/stubs/wit_world/exports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import weakref

from componentize_py_types import Result, Ok, Err, Some
from ..imports import http_req
from ..imports import async_io
from ..imports import http_req

class HttpIncoming(Protocol):

Expand All @@ -26,7 +26,7 @@ def handle(self, request: http_req.Request, body: async_io.Pollable) -> None:
stream the response body after the response has been initiated, use
`send-downstream-streaming`.

Raises: `wit_world.types.Err(None)`
Raises: `componentize_py_types.Err(None)`
"""
raise NotImplementedError

Expand Down
6 changes: 3 additions & 3 deletions fastly_compute/testing/stubs/wit_world/imports/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import weakref

from componentize_py_types import Result, Ok, Err, Some
from ..imports import async_io
from ..imports import types
from ..imports import async_io

class AclError(Enum):
"""
Expand All @@ -33,7 +33,7 @@ def open(cls, name: str) -> Self:
"""
Opens an ACL linked to the current service with the given link name.

Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)`
Raises: `componentize_py_types.Err(wit_world.imports.types.OpenError)`
"""
raise NotImplementedError
def lookup(self, ip_addr: types.IpAddress) -> Optional[async_io.Pollable]:
Expand All @@ -45,7 +45,7 @@ def lookup(self, ip_addr: types.IpAddress) -> Optional[async_io.Pollable]:
If no matches are found, then `ok(none)` is returned. This corresponds
to an HTTP error code of 204, “No Content”.

Raises: `wit_world.types.Err(wit_world.imports.acl.AclError)`
Raises: `componentize_py_types.Err(wit_world.imports.acl.AclError)`
"""
raise NotImplementedError
def __enter__(self) -> Self:
Expand Down
36 changes: 18 additions & 18 deletions fastly_compute/testing/stubs/wit_world/imports/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ def open(cls, name: str) -> Self:
"""
Attempts to open the named static backend.

Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)`
Raises: `componentize_py_types.Err(wit_world.imports.types.OpenError)`
"""
raise NotImplementedError
def get_name(self) -> str:
Expand All @@ -283,21 +283,21 @@ def is_healthy(self) -> BackendHealth:
For backends without a configured healthcheck, this will always return
`backend-health.unknown`.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def is_dynamic(self) -> bool:
"""
Returns `true` if the backend is a “dynamic” backend.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_host(self, max_len: int) -> str:
"""
Gets the host of this backend.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_override_host(self, max_len: int) -> Optional[bytes]:
Expand All @@ -309,21 +309,21 @@ def get_override_host(self, max_len: int) -> Optional[bytes]:

[the Fastly documentation on override hosts]: https://docs.fastly.com/en/guides/specifying-an-override-host

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_port(self) -> int:
"""
Gets the remote TCP port of the backend connection for the request.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_connect_timeout_ms(self) -> int:
"""
Gets the connection timeout of the backend.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_first_byte_timeout_ms(self) -> int:
Expand All @@ -332,7 +332,7 @@ def get_first_byte_timeout_ms(self) -> int:

This timeout applies between the time of connection and the time we get the first byte back.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_between_bytes_timeout_ms(self) -> int:
Expand All @@ -341,66 +341,66 @@ def get_between_bytes_timeout_ms(self) -> int:

This timeout applies between any two bytes we receive across the wire.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def is_tls(self) -> bool:
"""
Returns `true` if the backend is configured to use TLS.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_tls_min_version(self) -> Optional[int]:
"""
Gets the minimum TLS version this backend will use.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_tls_max_version(self) -> Optional[int]:
"""
Gets the maximum TLS version this backend will use.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_http_keepalive_time(self) -> int:
"""
Returns the time for this backend to hold onto an idle HTTP keepalive connection
after it was last used before closing it.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_tcp_keepalive_enable(self) -> bool:
"""
Returns `true` if TCP keepalives have been enabled for this backend.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_tcp_keepalive_interval(self) -> int:
"""
Returns the time to wait in between sending each TCP keepalive probe to this backend.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_tcp_keepalive_probes(self) -> int:
"""
Returns the time to wait after the last data was sent before starting to send TCP keepalive
probes to this backend.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def get_tcp_keepalive_time(self) -> int:
"""
Returns the time to wait after the last data was sent before starting to send TCP keepalive
probes to this backend.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
def __enter__(self) -> Self:
Expand Down Expand Up @@ -439,6 +439,6 @@ def register_dynamic_backend(prefix: str, target: str, options: DynamicBackendOp
backends with a service that has not had dynamic backends enabled, or dynamic backends have
been administratively prohibited for the node in response to an ongoing incident.

Raises: `wit_world.types.Err(wit_world.imports.types.Error)`
Raises: `componentize_py_types.Err(wit_world.imports.types.Error)`
"""
raise NotImplementedError
Loading
Loading