From a2bde007fe6f221b6dbc6973eadc52735147e54e Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 31 Mar 2026 16:11:13 -0400 Subject: [PATCH 1/2] Correct some docs and get rid of an unnecessary rename. --- fastly_compute/wsgi.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fastly_compute/wsgi.py b/fastly_compute/wsgi.py index ead6900..d68eb03 100644 --- a/fastly_compute/wsgi.py +++ b/fastly_compute/wsgi.py @@ -15,7 +15,7 @@ from urllib.parse import urlparse from wsgiref.types import InputStream -from wit_world.exports import HttpIncoming as WitHttpIncoming +from wit_world.exports import HttpIncoming from wit_world.imports import async_io, http_body, http_req, http_resp from wit_world.imports.http_downstream import ( NextRequestOptions, @@ -156,12 +156,11 @@ def start_response( send_downstream(error_response, error_body) -class WsgiHttpIncoming(WitHttpIncoming): +class WsgiHttpIncoming(HttpIncoming): """HTTP request handler that serves WSGI applications. This class provides a convenient base class for serving WSGI applications - on Fastly Compute. Subclass this and set the `app` attribute to your WSGI - application. + on Fastly Compute. Instantiate this, and pass in your application. Example:: From b5266ec21e58a4b3357f7a4749559e68b5b7e758 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 31 Mar 2026 16:09:48 -0400 Subject: [PATCH 2/2] Check in `stubs` folder. Closes #46. I figure putting this in `testing/` suggests what it's supposed to be used for. And we'll document it, of course. --- .gitignore | 3 - Makefile | 2 +- .../componentize_py_async_support/__init__.py | 529 ++++++++++++++++ .../componentize_py_async_support/futures.py | 93 +++ .../componentize_py_async_support/streams.py | 189 ++++++ .../testing/stubs/componentize_py_runtime.pyi | 34 ++ .../testing/stubs/componentize_py_types.py | 19 + fastly_compute/testing/stubs/poll_loop.py | 441 ++++++++++++++ .../testing/stubs/wit_world/__init__.py | 15 + .../stubs/wit_world/exports/__init__.py | 33 + .../stubs/wit_world/exports/http_incoming.py | 21 + .../stubs/wit_world/imports/__init__.py | 0 .../testing/stubs/wit_world/imports/acl.py | 62 ++ .../stubs/wit_world/imports/async_io.py | 105 ++++ .../stubs/wit_world/imports/backend.py | 444 ++++++++++++++ .../testing/stubs/wit_world/imports/cache.py | 485 +++++++++++++++ .../wit_world/imports/compute_runtime.py | 151 +++++ .../stubs/wit_world/imports/config_store.py | 51 ++ .../wit_world/imports/device_detection.py | 28 + .../stubs/wit_world/imports/dictionary.py | 54 ++ .../stubs/wit_world/imports/environment.py | 36 ++ .../testing/stubs/wit_world/imports/erl.py | 128 ++++ .../testing/stubs/wit_world/imports/error.py | 56 ++ .../testing/stubs/wit_world/imports/exit.py | 18 + .../testing/stubs/wit_world/imports/geo.py | 30 + .../stubs/wit_world/imports/http_body.py | 155 +++++ .../stubs/wit_world/imports/http_cache.py | 448 ++++++++++++++ .../wit_world/imports/http_downstream.py | 239 ++++++++ .../stubs/wit_world/imports/http_req.py | 565 ++++++++++++++++++ .../stubs/wit_world/imports/http_resp.py | 209 +++++++ .../stubs/wit_world/imports/http_types.py | 55 ++ .../wit_world/imports/image_optimizer.py | 47 ++ .../stubs/wit_world/imports/insecure.py | 39 ++ .../stubs/wit_world/imports/insecure_seed.py | 40 ++ .../stubs/wit_world/imports/kv_store.py | 349 +++++++++++ .../testing/stubs/wit_world/imports/log.py | 59 ++ .../wit_world/imports/monotonic_clock.py | 49 ++ .../testing/stubs/wit_world/imports/poll.py | 72 +++ .../testing/stubs/wit_world/imports/purge.py | 63 ++ .../testing/stubs/wit_world/imports/random.py | 43 ++ .../stubs/wit_world/imports/secret_store.py | 95 +++ .../stubs/wit_world/imports/security.py | 57 ++ .../stubs/wit_world/imports/shielding.py | 51 ++ .../testing/stubs/wit_world/imports/stderr.py | 15 + .../testing/stubs/wit_world/imports/stdin.py | 15 + .../testing/stubs/wit_world/imports/stdout.py | 15 + .../stubs/wit_world/imports/streams.py | 330 ++++++++++ .../testing/stubs/wit_world/imports/types.py | 116 ++++ .../stubs/wit_world/imports/wall_clock.py | 61 ++ fastly_compute/tests/__init__.py | 2 +- pyproject.toml | 4 +- 51 files changed, 6214 insertions(+), 6 deletions(-) create mode 100644 fastly_compute/testing/stubs/componentize_py_async_support/__init__.py create mode 100644 fastly_compute/testing/stubs/componentize_py_async_support/futures.py create mode 100644 fastly_compute/testing/stubs/componentize_py_async_support/streams.py create mode 100644 fastly_compute/testing/stubs/componentize_py_runtime.pyi create mode 100644 fastly_compute/testing/stubs/componentize_py_types.py create mode 100644 fastly_compute/testing/stubs/poll_loop.py create mode 100644 fastly_compute/testing/stubs/wit_world/__init__.py create mode 100644 fastly_compute/testing/stubs/wit_world/exports/__init__.py create mode 100644 fastly_compute/testing/stubs/wit_world/exports/http_incoming.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/__init__.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/acl.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/async_io.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/backend.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/cache.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/compute_runtime.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/config_store.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/device_detection.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/dictionary.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/environment.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/erl.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/error.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/exit.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/geo.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/http_body.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/http_cache.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/http_downstream.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/http_req.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/http_resp.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/http_types.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/image_optimizer.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/insecure.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/insecure_seed.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/kv_store.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/log.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/monotonic_clock.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/poll.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/purge.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/random.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/secret_store.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/security.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/shielding.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/stderr.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/stdin.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/stdout.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/streams.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/types.py create mode 100644 fastly_compute/testing/stubs/wit_world/imports/wall_clock.py diff --git a/.gitignore b/.gitignore index 8543346..768d3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ *.egg-info __pycache__ -# Generated code -/stubs/ - # Build artifacts /build/ bin/ diff --git a/Makefile b/Makefile index 00d5407..fc90f60 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ else endif # Configuration -STUBS_DIR := stubs +STUBS_DIR := fastly_compute/testing/stubs BUILD_DIR := build EXAMPLES_DIR := examples COMPUTE_WIT := wit/deps/fastly/compute.wit diff --git a/fastly_compute/testing/stubs/componentize_py_async_support/__init__.py b/fastly_compute/testing/stubs/componentize_py_async_support/__init__.py new file mode 100644 index 0000000..a7bd016 --- /dev/null +++ b/fastly_compute/testing/stubs/componentize_py_async_support/__init__.py @@ -0,0 +1,529 @@ +import asyncio +import componentize_py_runtime +import subprocess + +from os import PathLike +from socket import AddressFamily, AddressInfo, SocketKind, socket +from concurrent.futures import Executor +from componentize_py_types import Result, Ok, Err +from dataclasses import dataclass +from typing import Any, Optional, TypeVar, TypeVarTuple, Callable, IO, Literal, cast +from contextvars import ContextVar, Context +from collections.abc import Coroutine, Awaitable, Sequence +from asyncio.protocols import BaseProtocol +from asyncio.transports import Transport, WriteTransport, DatagramTransport, SubprocessTransport, ReadTransport +from asyncio.base_events import Server + +try: + from ssl import SSLContext +except: + pass + +@dataclass +class _FutureState: + waitable_set: int | None + futures: dict[int, Any] + handles: list[asyncio.Handle] + pending_count: int + +class _ReturnCode: + COMPLETED = 0 + DROPPED = 1 + CANCELLED = 2 + +class _CallbackCode: + EXIT = 0 + YIELD = 1 + WAIT = 2 + POLL = 3 + +class _Event: + NONE = 0 + SUBTASK = 1 + STREAM_READ = 2 + STREAM_WRITE = 3 + FUTURE_READ = 4 + FUTURE_WRITE = 5 + CANCELLED = 6 + +class _Status: + STARTING = 0 + STARTED = 1 + RETURNED = 2 + START_CANCELLED = 3 + RETURN_CANCELLED = 4 + +_T = TypeVar("_T") +_Ts = TypeVarTuple("_Ts") +_ProtocolT = TypeVar("_ProtocolT", bound=BaseProtocol) + +async def _noop() -> None: + pass + +class _Loop(asyncio.AbstractEventLoop): + def __init__(self) -> None: + self.running: bool = False + self.exception: Optional[Any] = None + + def poll(self, future_state: _FutureState) -> None: + while True: + handles = future_state.handles + future_state.handles = [] + for handle in handles: + if not handle._cancelled: + handle._run() + + if self.exception is not None: + raise self.exception + + if len(handles) == 0 and len(future_state.handles) == 0: + return + + def get_debug(self) -> bool: + return False + + def is_running(self) -> bool: + return self.running + + def is_closed(self) -> bool: + return not self.running + + def stop(self) -> None: + self.running = False + + def close(self) -> None: + self.running = False + + def shutdown_asyncgens(self) -> Coroutine[Any, Any, None]: + return _noop() + + def call_exception_handler(self, context: dict[str, Any]) -> None: + self.exception = context.get('exception', None) + + def call_soon(self, + callback: Callable[[*_Ts], object], + *args: *_Ts, + context: Context | None = None) -> asyncio.Handle: + global _future_state + + if context is not None: + future_state = context[_future_state] + handle = asyncio.Handle(callback, args, self, context) + future_state.handles.append(handle) + return handle + else: + raise AssertionError + + def create_task(self, coroutine: Coroutine[Any, Any, _T], name: str | None = None, context: Context | None = None) -> asyncio.Task[_T]: + return asyncio.Task(coroutine, loop=self, context=context) + + def create_future(self) -> asyncio.Future[Any]: + return asyncio.Future(loop=self) + + # The remaining methods should be irrelevant for our purposes and thus unimplemented + + def run_until_complete(self, future: Awaitable[_T]) -> _T: + raise NotImplementedError + + def run_forever(self) -> None: + raise NotImplementedError + + async def shutdown_default_executor(self) -> None: + raise NotImplementedError + + def call_later(self, + delay: float, + callback: Callable[[*_Ts], object], + *args: *_Ts, + context: Context | None = None) -> asyncio.TimerHandle: + raise NotImplementedError + + def call_at(self, + when: float, + callback: Callable[[*_Ts], object], + *args: *_Ts, + context: Context | None = None) -> asyncio.TimerHandle: + raise NotImplementedError + + def time(self) -> float: + raise NotImplementedError + + def call_soon_threadsafe(self, + callback: Callable[[*_Ts], object], + *args: *_Ts, + context: Context | None = None) -> asyncio.Handle: + raise NotImplementedError + + def run_in_executor(self, + executor: Executor | None, + func: Callable[[*_Ts], _T], + *args: *_Ts) -> asyncio.Future[_T]: + raise NotImplementedError + + def set_default_executor(self, executor: Executor) -> None: + raise NotImplementedError + + async def getaddrinfo( + self, + host: bytes | str | None, + port: bytes | str | int | None, + *, + family: int = 0, + type: int = 0, + proto: int = 0, + flags: int = 0, + ) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int]]]: + raise NotImplementedError + + async def getnameinfo(self, + sockaddr: tuple[str, int] | tuple[str, int, int, int], + flags: int = 0) -> tuple[str, str]: + raise NotImplementedError + + async def create_connection( + self, + protocol_factory: Callable[[], _ProtocolT], + host: str | None = ..., + port: int | None = ..., + *, + ssl: bool | SSLContext | None = None, + family: int = 0, + proto: int = 0, + flags: int = 0, + sock: socket | None = None, + local_addr: tuple[str, int] | None = None, + server_hostname: str | None = None, + ssl_handshake_timeout: float | None = None, + ssl_shutdown_timeout: float | None = None, + happy_eyeballs_delay: float | None = None, + interleave: int | None = None, + ) -> tuple[Transport, _ProtocolT]: + raise NotImplementedError + + async def create_server( + self, + protocol_factory: Callable[[], BaseProtocol], + host: str | Sequence[str] | None = None, + port: int | None = None, + *, + family: int = AddressFamily.AF_UNSPEC, + flags: int = AddressInfo.AI_PASSIVE, + sock: socket | None = ..., + backlog: int = 100, + ssl: bool | SSLContext | None = None, + reuse_address: bool | None = None, + reuse_port: bool | None = None, + keep_alive: bool | None = None, + ssl_handshake_timeout: float | None = None, + ssl_shutdown_timeout: float | None = None, + start_serving: bool = True, + ) -> Server: + raise NotImplementedError + + async def sendfile(self, + transport: WriteTransport, + file: IO[bytes], + offset: int = 0, + count: int | None = None, + *, + fallback: bool = True + ) -> int: + raise NotImplementedError + + async def start_tls( + self, + transport: WriteTransport, + protocol: BaseProtocol, + sslcontext: SSLContext, + *, + server_side: bool = False, + server_hostname: str | None = None, + ssl_handshake_timeout: float | None = None, + ssl_shutdown_timeout: float | None = None, + ) -> Transport | None: + raise NotImplementedError + + async def create_unix_connection( + self, + protocol_factory: Callable[[], _ProtocolT], + path: str | None = None, + *, + ssl: bool | SSLContext | None = None, + sock: socket | None = None, + server_hostname: str | None = None, + ssl_handshake_timeout: float | None = None, + ssl_shutdown_timeout: float | None = None, + ) -> tuple[Transport, _ProtocolT]: + raise NotImplementedError + + async def create_unix_server( + self, + protocol_factory: Callable[[], BaseProtocol], + path: str | PathLike[str] | None = None, + *, + sock: socket | None = None, + backlog: int = 100, + ssl: bool | SSLContext | None = None, + ssl_handshake_timeout: float | None = None, + ssl_shutdown_timeout: float | None = None, + start_serving: bool = True, + ) -> Server: + raise NotImplementedError + + async def connect_accepted_socket( + self, + protocol_factory: Callable[[], _ProtocolT], + sock: socket, + *, + ssl: bool | SSLContext | None = None, + ssl_handshake_timeout: float | None = None, + ssl_shutdown_timeout: float | None = None, + ) -> tuple[Transport, _ProtocolT]: + raise NotImplementedError + + async def create_datagram_endpoint( + self, + protocol_factory: Callable[[], _ProtocolT], + local_addr: tuple[str, int] | str | None = None, + remote_addr: tuple[str, int] | str | None = None, + *, + family: int = 0, + proto: int = 0, + flags: int = 0, + reuse_address: bool | None = None, + reuse_port: bool | None = None, + allow_broadcast: bool | None = None, + sock: socket | None = None, + ) -> tuple[DatagramTransport, _ProtocolT]: + raise NotImplementedError + + async def connect_read_pipe(self, + protocol_factory: Callable[[], _ProtocolT], + pipe: Any) -> tuple[ReadTransport, _ProtocolT]: + raise NotImplementedError + + async def connect_write_pipe(self, + protocol_factory: Callable[[], _ProtocolT], + pipe: Any) -> tuple[WriteTransport, _ProtocolT]: + raise NotImplementedError + + async def subprocess_shell( + self, + protocol_factory: Callable[[], _ProtocolT], + cmd: bytes | str, + *, + stdin: int | IO[Any] | None = -1, + stdout: int | IO[Any] | None = -1, + stderr: int | IO[Any] | None = -1, + universal_newlines: Literal[False] = False, + shell: Literal[True] = True, + bufsize: Literal[0] = 0, + encoding: None = None, + errors: None = None, + text: Literal[False] | None = None, + **kwargs: Any, + ) -> tuple[SubprocessTransport, _ProtocolT]: + raise NotImplementedError + + async def subprocess_exec( + self, + protocol_factory: Callable[[], _ProtocolT], + program: Any, + *args: Any, + stdin: int | IO[Any] | None = -1, + stdout: int | IO[Any] | None = -1, + stderr: int | IO[Any] | None = -1, + universal_newlines: Literal[False] = False, + shell: Literal[False] = False, + bufsize: Literal[0] = 0, + encoding: None = None, + errors: None = None, + **kwargs: Any, + ) -> tuple[SubprocessTransport, _ProtocolT]: + raise NotImplementedError + + def add_reader(self, fd: Any, callback: Callable[[*_Ts], Any], *args: *_Ts) -> None: + raise NotImplementedError + + def remove_reader(self, fd: Any) -> bool: + raise NotImplementedError + + def add_writer(self, fd: Any, callback: Callable[[*_Ts], Any], *args: *_Ts) -> None: + raise NotImplementedError + + def remove_writer(self, fd: Any) -> bool: + raise NotImplementedError + + async def sock_recv(self, sock: socket, nbytes: int) -> bytes: + raise NotImplementedError + + async def sock_recv_into(self, sock: socket, buf: Any) -> int: + raise NotImplementedError + + async def sock_recvfrom(self, sock: socket, bufsize: int) -> tuple[bytes, Any]: + raise NotImplementedError + + async def sock_recvfrom_into(self, sock: socket, buf: Any, nbytes: int = 0) -> tuple[int, Any]: + raise NotImplementedError + + async def sock_sendall(self, sock: socket, data: Any) -> None: + raise NotImplementedError + + async def sock_sendto(self, sock: socket, data: Any, address: Any) -> int: + raise NotImplementedError + + async def sock_connect(self, sock: socket, address: Any) -> None: + raise NotImplementedError + + async def sock_accept(self, sock: socket) -> tuple[socket, Any]: + raise NotImplementedError + + async def sock_sendfile( + self, + sock: socket, + file: IO[bytes], + offset: int = 0, + count: int | None = None, + *, + fallback: bool | None = None + ) -> int: + raise NotImplementedError + + def add_signal_handler(self, sig: int, callback: Callable[[*_Ts], object], *args: *_Ts) -> None: + raise NotImplementedError + + def remove_signal_handler(self, sig: int) -> bool: + raise NotImplementedError + + def set_task_factory(self, factory: Any | None) -> None: + raise NotImplementedError + + def get_task_factory(self) -> None: + raise NotImplementedError + + def get_exception_handler(self) -> None: + raise NotImplementedError + + def set_exception_handler(self, handler: Any | None) -> None: + raise NotImplementedError + + def default_exception_handler(self, context: dict[str, Any]) -> None: + raise NotImplementedError + + def set_debug(self, enabled: bool) -> None: + raise NotImplementedError + +_future_state: ContextVar[_FutureState] = ContextVar("_future_state") +_loop = _Loop() +asyncio.set_event_loop(_loop) +_loop.running = True +asyncio.events._set_running_loop(_loop) + +def _set_future_state(future_state: _FutureState) -> None: + global _future_state + + _future_state.set(future_state) + +async def _return_result(export_index: int, borrows: int, coroutine: Any) -> None: + global _future_state + + try: + try: + result: Result[Any, Any] = Ok(await coroutine) + except Err as e: + result = e + + componentize_py_runtime.call_task_return(export_index, borrows, result) + except Exception as e: + _loop.exception = e + + assert _future_state.get().pending_count > 0 + _future_state.get().pending_count -= 1 + +def first_poll(export_index: int, borrows: int, coroutine: Any) -> int: + context = Context() + future_state = _FutureState(None, {}, [], 1) + context.run(_set_future_state, future_state) + asyncio.create_task(_return_result(export_index, borrows, coroutine), context=context) + return _poll(future_state) + +def _poll(future_state: _FutureState) -> int: + global _loop + + _loop.poll(future_state) + + if future_state.pending_count == 0: + if future_state.waitable_set is not None: + componentize_py_runtime.waitable_set_drop(future_state.waitable_set) + + return _CallbackCode.EXIT + else: + waitable_set = future_state.waitable_set + assert waitable_set is not None + componentize_py_runtime.context_set(future_state) + return _CallbackCode.WAIT | (waitable_set << 4) + +def callback(event0: int, event1: int, event2: int) -> int: + future_state = componentize_py_runtime.context_get() + componentize_py_runtime.context_set(None) + + match event0: + case _Event.NONE: + pass + case _Event.SUBTASK: + match event2: + case _Status.STARTING: + raise AssertionError + case _Status.STARTED: + pass + case _Status.RETURNED: + componentize_py_runtime.waitable_join(event1, 0) + componentize_py_runtime.subtask_drop(event1) + future_state.futures.pop(event1).set_result(event2) + case _: + # todo + raise NotImplementedError + case _Event.STREAM_READ | _Event.STREAM_WRITE | _Event.FUTURE_READ | _Event.FUTURE_WRITE: + componentize_py_runtime.waitable_join(event1, 0) + future_state.futures.pop(event1).set_result(event2) + case _: + # todo + raise NotImplementedError + + return _poll(future_state) + +async def await_result[T](result: Result[T, tuple[int, int]]) -> T: + global _loop + global _future_state + + if isinstance(result, Ok): + return result.value + else: + future_state = _future_state.get() + waitable, promise = result.value + future = _loop.create_future() + future_state.futures[waitable] = future + + if future_state.waitable_set is None: + future_state.waitable_set = componentize_py_runtime.waitable_set_new() + componentize_py_runtime.waitable_join(waitable, future_state.waitable_set) + + return cast(T, componentize_py_runtime.promise_get_result(await future, promise)) + +async def _wrap_spawned(coroutine: Any) -> None: + global _future_state + + try: + await coroutine + except Exception as e: + _loop.exception = e + + assert _future_state.get().pending_count > 0 + _future_state.get().pending_count -= 1 + +def spawn(coroutine: Any) -> None: + global _future_state + + _future_state.get().pending_count += 1 + + asyncio.create_task(_wrap_spawned(coroutine)) diff --git a/fastly_compute/testing/stubs/componentize_py_async_support/futures.py b/fastly_compute/testing/stubs/componentize_py_async_support/futures.py new file mode 100644 index 0000000..a50c16e --- /dev/null +++ b/fastly_compute/testing/stubs/componentize_py_async_support/futures.py @@ -0,0 +1,93 @@ +import componentize_py_runtime +import componentize_py_async_support +import weakref + +from typing import TypeVar, Generic, cast, Self, Any, Callable +from types import TracebackType +from componentize_py_async_support import _ReturnCode + +T = TypeVar('T') + +class FutureReader(Generic[T]): + def __init__(self, type_: int, handle: int): + self.type_ = type_ + self.handle: int | None = handle + self.finalizer = weakref.finalize(self, componentize_py_runtime.future_drop_readable, type_, handle) + + async def read(self) -> T: + self.finalizer.detach() + handle = self.handle + self.handle = None + if handle is not None: + result = await componentize_py_async_support.await_result( + componentize_py_runtime.future_read(self.type_, handle) + ) + componentize_py_runtime.future_drop_readable(self.type_, handle) + return cast(T, result) + else: + raise AssertionError + + def __enter__(self) -> Self: + return self + + def __exit__(self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None) -> bool | None: + if self.handle is not None: + self.finalizer.detach() + handle = self.handle + self.handle = None + componentize_py_runtime.future_drop_readable(self.type_, handle) + + return None + +async def write(type_: int, handle: int, value: Any) -> None: + await componentize_py_async_support.await_result( + componentize_py_runtime.future_write(type_, handle, value) + ) + componentize_py_runtime.future_drop_writable(type_, handle) + +def write_default(type_: int, handle: int, default: Callable[[], Any]) -> None: + componentize_py_async_support.spawn(write(type_, handle, default())) + +class FutureWriter(Generic[T]): + def __init__(self, type_: int, handle: int, default: Callable[[], T]): + self.type_ = type_ + self.handle: int | None = handle + self.default = default + self.finalizer = weakref.finalize(self, write_default, type_, handle, default) + + async def write(self, value: T) -> bool: + self.finalizer.detach() + handle = self.handle + self.handle = None + if handle is not None: + code, _ = await componentize_py_async_support.await_result( + componentize_py_runtime.future_write(self.type_, handle, value) + ) + componentize_py_runtime.future_drop_writable(self.type_, handle) + match code: + case _ReturnCode.COMPLETED: + return True + case _ReturnCode.DROPPED: + return False + case _ReturnCode.CANCELLED: + # todo + raise NotImplementedError + case _: + raise AssertionError + else: + raise AssertionError + + def __enter__(self) -> Self: + return self + + def __exit__(self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None) -> bool | None: + if self.handle is not None: + componentize_py_async_support.spawn(self.write(self.default())) + + return None diff --git a/fastly_compute/testing/stubs/componentize_py_async_support/streams.py b/fastly_compute/testing/stubs/componentize_py_async_support/streams.py new file mode 100644 index 0000000..7650d62 --- /dev/null +++ b/fastly_compute/testing/stubs/componentize_py_async_support/streams.py @@ -0,0 +1,189 @@ +import componentize_py_runtime +import componentize_py_async_support +import weakref + +from typing import TypeVar, Generic, Self, cast +from types import TracebackType +from componentize_py_async_support import _ReturnCode + +class ByteStreamReader: + def __init__(self, type_: int, handle: int): + self.writer_dropped = False + self.type_ = type_ + self.handle: int | None = handle + self.finalizer = weakref.finalize(self, componentize_py_runtime.stream_drop_readable, type_, handle) + + async def read(self, max_count: int) -> bytes: + if self.writer_dropped: + return bytes() + + code, values = await self._read(max_count) + + if code == _ReturnCode.DROPPED: + self.writer_dropped = True + + return values + + async def _read(self, max_count: int) -> tuple[int, bytes]: + if self.handle is not None: + return cast(tuple[int, bytes], await componentize_py_async_support.await_result( + componentize_py_runtime.stream_read(self.type_, self.handle, max_count) + )) + else: + raise AssertionError + + def __enter__(self) -> Self: + return self + + def __exit__(self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None) -> bool | None: + self.finalizer.detach() + handle = self.handle + self.handle = None + if handle is not None: + componentize_py_runtime.stream_drop_readable(self.type_, handle) + return None + +class ByteStreamWriter: + def __init__(self, type_: int, handle: int): + self.reader_dropped = False + self.type_ = type_ + self.handle: int | None = handle + self.finalizer = weakref.finalize(self, componentize_py_runtime.stream_drop_writable, type_, handle) + + async def write(self, source: bytes) -> int: + if self.reader_dropped: + return 0 + + code, count = await self._write(source) + + if code == _ReturnCode.DROPPED: + self.reader_dropped = True + + return count + + async def _write(self, source: bytes) -> tuple[int, int]: + if self.handle is not None: + return await componentize_py_async_support.await_result( + componentize_py_runtime.stream_write(self.type_, self.handle, source) + ) + else: + raise AssertionError + + async def write_all(self, source: bytes) -> int: + total = 0 + + while len(source) > 0 and not self.reader_dropped: + count = await self.write(source) + source = source[count:] + total += count + + return total + + def __enter__(self) -> Self: + return self + + def __exit__(self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None) -> bool | None: + self.finalizer.detach() + handle = self.handle + self.handle = None + if handle is not None: + componentize_py_runtime.stream_drop_writable(self.type_, handle) + return None + +T = TypeVar('T') + +class StreamReader(Generic[T]): + def __init__(self, type_: int, handle: int): + self.writer_dropped = False + self.type_ = type_ + self.handle: int | None = handle + self.finalizer = weakref.finalize(self, componentize_py_runtime.stream_drop_readable, type_, handle) + + async def read(self, max_count: int) -> list[T]: + if self.writer_dropped: + return [] + + code, values = await self._read(max_count) + + if code == _ReturnCode.DROPPED: + self.writer_dropped = True + + return values + + async def _read(self, max_count: int) -> tuple[int, list[T]]: + if self.handle is not None: + return cast(tuple[int, list[T]], await componentize_py_async_support.await_result( + componentize_py_runtime.stream_read(self.type_, self.handle, max_count) + )) + else: + raise AssertionError + + def __enter__(self) -> Self: + return self + + def __exit__(self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None) -> bool | None: + self.finalizer.detach() + handle = self.handle + self.handle = None + if handle is not None: + componentize_py_runtime.stream_drop_readable(self.type_, handle) + return None + +class StreamWriter(Generic[T]): + def __init__(self, type_: int, handle: int): + self.reader_dropped = False + self.type_ = type_ + self.handle: int | None = handle + self.finalizer = weakref.finalize(self, componentize_py_runtime.stream_drop_writable, type_, handle) + + async def write(self, source: list[T]) -> int: + if self.reader_dropped: + return 0 + + code, count = await self._write(source) + + if code == _ReturnCode.DROPPED: + self.reader_dropped = True + + return count + + async def _write(self, source: list[T]) -> tuple[int, int]: + if self.handle is not None: + return await componentize_py_async_support.await_result( + componentize_py_runtime.stream_write(self.type_, self.handle, source) + ) + else: + raise AssertionError + + async def write_all(self, source: list[T]) -> int: + total = 0 + + while len(source) > 0 and not self.reader_dropped: + count = await self.write(source) + source = source[count:] + total += count + + return total + + def __enter__(self) -> Self: + return self + + def __exit__(self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None) -> bool | None: + self.finalizer.detach() + handle = self.handle + self.handle = None + if handle is not None: + componentize_py_runtime.stream_drop_writable(self.type_, handle) + return None diff --git a/fastly_compute/testing/stubs/componentize_py_runtime.pyi b/fastly_compute/testing/stubs/componentize_py_runtime.pyi new file mode 100644 index 0000000..ff09cf8 --- /dev/null +++ b/fastly_compute/testing/stubs/componentize_py_runtime.pyi @@ -0,0 +1,34 @@ +from typing import Any +from componentize_py_types import Result + +def call_task_return(index: int, borrows: int, result: Any) -> None: ... + +def waitable_set_drop(set: int) -> None: ... + +def context_set(value: Any) -> None: ... + +def context_get() -> Any: ... + +def waitable_join(waitable: int, set: int) -> None: ... + +def subtask_drop(task: int) -> None: ... + +def waitable_set_new() -> int: ... + +def promise_get_result(event: int, promise: int) -> Any: ... + +def future_read(ty: int, future: int) -> Result[Any, tuple[int, int]]: ... + +def future_write(ty: int, future: int, value: Any) -> Result[tuple[int, int], tuple[int, int]]: ... + +def future_drop_readable(ty: int, future: int) -> None: ... + +def future_drop_writable(ty: int, future: int) -> None: ... + +def stream_read(ty: int, stream: int, max_count: int) -> Result[tuple[int, bytes | list[Any]], tuple[int, int]]: ... + +def stream_write(ty: int, stream: int, values: bytes | list[Any]) -> Result[tuple[int, int], tuple[int, int]]: ... + +def stream_drop_readable(ty: int, stream: int) -> None: ... + +def stream_drop_writable(ty: int, stream: int) -> None: ... diff --git a/fastly_compute/testing/stubs/componentize_py_types.py b/fastly_compute/testing/stubs/componentize_py_types.py new file mode 100644 index 0000000..9867d14 --- /dev/null +++ b/fastly_compute/testing/stubs/componentize_py_types.py @@ -0,0 +1,19 @@ +from typing import TypeVar, Generic, Union +from dataclasses import dataclass + +S = TypeVar('S') +@dataclass +class Some(Generic[S]): + value: S + +T = TypeVar('T') +@dataclass +class Ok(Generic[T]): + value: T + +E = TypeVar('E') +@dataclass(frozen=True) +class Err(Generic[E], Exception): + value: E + +Result = Union[Ok[T], Err[E]] diff --git a/fastly_compute/testing/stubs/poll_loop.py b/fastly_compute/testing/stubs/poll_loop.py new file mode 100644 index 0000000..c067a0f --- /dev/null +++ b/fastly_compute/testing/stubs/poll_loop.py @@ -0,0 +1,441 @@ +"""Defines a custom `asyncio` event loop backed by `wasi:io/poll#poll`. + +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. +""" + +import asyncio +import socket +import subprocess + +from componentize_py_types import Ok, Err +from wit_world.imports import types, streams, poll, outgoing_handler +from wit_world.imports.types import ( + IncomingBody, + OutgoingBody, + OutgoingRequest, + IncomingResponse, +) +from wit_world.imports.streams import StreamError_Closed, InputStream +from wit_world.imports.poll import Pollable +from typing import Optional, cast + +# Maximum number of bytes to read at a time +READ_SIZE: int = 16 * 1024 + + +async def send(request: OutgoingRequest) -> IncomingResponse: + """Send the specified request and wait asynchronously for the response.""" + + future = outgoing_handler.handle(request, None) + + while True: + response = future.get() + if response is None: + await register(cast(PollLoop, asyncio.get_event_loop()), future.subscribe()) + else: + if isinstance(response, Ok): + if isinstance(response.value, Ok): + return response.value.value + else: + raise response.value + else: + raise response + + +class Stream: + """Reader abstraction over `wasi:http/types#incoming-body`.""" + + def __init__(self, body: IncomingBody): + self.body: Optional[IncomingBody] = body + self.stream: Optional[InputStream] = body.stream() + + async def next(self) -> Optional[bytes]: + """Wait for the next chunk of data to arrive on the stream. + + This will return `None` when the end of the stream has been reached. + """ + while True: + try: + if self.stream is None: + return None + else: + buffer = self.stream.read(READ_SIZE) + if len(buffer) == 0: + await register( + cast(PollLoop, asyncio.get_event_loop()), + self.stream.subscribe(), + ) + else: + return buffer + except Err as e: + if isinstance(e.value, StreamError_Closed): + if self.stream is not None: + self.stream.__exit__(None, None, None) + self.stream = None + if self.body is not None: + IncomingBody.finish(self.body) + self.body = None + else: + raise e + + +class Sink: + """Writer abstraction over `wasi:http/types#outgoing-body`.""" + + def __init__(self, body: OutgoingBody): + self.body = body + self.stream = body.write() + + async def send(self, chunk: bytes): + """Write the specified bytes to the sink. + + This may need to yield according to the backpressure requirements of the sink. + """ + offset = 0 + flushing = False + while True: + count = self.stream.check_write() + if count == 0: + await register( + cast(PollLoop, asyncio.get_event_loop()), self.stream.subscribe() + ) + elif offset == len(chunk): + if flushing: + return + else: + self.stream.flush() + flushing = True + else: + count = min(count, len(chunk) - offset) + self.stream.write(chunk[offset : offset + count]) + offset += count + + def close(self): + """Close the stream, indicating no further data will be written.""" + + self.stream.__exit__(None, None, None) + self.stream = None + OutgoingBody.finish(self.body, None) + self.body = None + + +class PollLoop(asyncio.AbstractEventLoop): + """Custom `asyncio` event loop backed by `wasi:io/poll#poll`.""" + + def __init__(self): + self.wakers = [] + self.running = False + self.handles = [] + self.exception = None + + def get_debug(self): + return False + + def run_until_complete(self, future): + future = asyncio.ensure_future(future, loop=self) + + self.running = True + asyncio.events._set_running_loop(self) + while self.running and not future.done(): + handles = self.handles + self.handles = [] + for handle in handles: + if not handle._cancelled: + handle._run() + + if self.wakers: + [pollables, wakers] = list(map(list, zip(*self.wakers))) + + new_wakers = [] + ready = [False] * len(pollables) + for index in poll.poll(pollables): + ready[index] = True + + for (ready, pollable), waker in zip(zip(ready, pollables), wakers): + if ready: + pollable.__exit__(None, None, None) + waker.set_result(None) + else: + new_wakers.append((pollable, waker)) + + self.wakers = new_wakers + + if self.exception is not None: + raise self.exception + + return future.result() + + def is_running(self): + return self.running + + def is_closed(self): + return not self.running + + def stop(self): + self.running = False + + def close(self): + self.running = False + + def shutdown_asyncgens(self): + pass + + def call_exception_handler(self, context): + self.exception = context.get("exception", None) + + def call_soon(self, callback, *args, context=None): + handle = asyncio.Handle(callback, args, self, context) + self.handles.append(handle) + return handle + + def create_task(self, coroutine): + return asyncio.Task(coroutine, loop=self) + + def create_future(self): + return asyncio.Future(loop=self) + + # The remaining methods should be irrelevant for our purposes and thus unimplemented + + def run_forever(self): + raise NotImplementedError + + async def shutdown_default_executor(self): + raise NotImplementedError + + def _timer_handle_cancelled(self, handle): + raise NotImplementedError + + def call_later(self, delay, callback, *args, context=None): + raise NotImplementedError + + def call_at(self, when, callback, *args, context=None): + raise NotImplementedError + + def time(self): + raise NotImplementedError + + def call_soon_threadsafe(self, callback, *args, context=None): + raise NotImplementedError + + def run_in_executor(self, executor, func, *args): + raise NotImplementedError + + def set_default_executor(self, executor): + raise NotImplementedError + + async def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0): + raise NotImplementedError + + async def getnameinfo(self, sockaddr, flags=0): + raise NotImplementedError + + async def create_connection( + self, + protocol_factory, + host=None, + port=None, + *, + ssl=None, + family=0, + proto=0, + flags=0, + sock=None, + local_addr=None, + server_hostname=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + happy_eyeballs_delay=None, + interleave=None, + ): + raise NotImplementedError + + async def create_server( + self, + protocol_factory, + host=None, + port=None, + *, + family=socket.AF_UNSPEC, + flags=socket.AI_PASSIVE, + sock=None, + backlog=100, + ssl=None, + reuse_address=None, + reuse_port=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + start_serving=True, + ): + raise NotImplementedError + + async def sendfile(self, transport, file, offset=0, count=None, *, fallback=True): + raise NotImplementedError + + async def start_tls( + self, + transport, + protocol, + sslcontext, + *, + server_side=False, + server_hostname=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + ): + raise NotImplementedError + + async def create_unix_connection( + self, + protocol_factory, + path=None, + *, + ssl=None, + sock=None, + server_hostname=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + ): + raise NotImplementedError + + async def create_unix_server( + self, + protocol_factory, + path=None, + *, + sock=None, + backlog=100, + ssl=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + start_serving=True, + ): + raise NotImplementedError + + async def connect_accepted_socket( + self, + protocol_factory, + sock, + *, + ssl=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + ): + raise NotImplementedError + + async def create_datagram_endpoint( + self, + protocol_factory, + local_addr=None, + remote_addr=None, + *, + family=0, + proto=0, + flags=0, + reuse_address=None, + reuse_port=None, + allow_broadcast=None, + sock=None, + ): + raise NotImplementedError + + async def connect_read_pipe(self, protocol_factory, pipe): + raise NotImplementedError + + async def connect_write_pipe(self, protocol_factory, pipe): + raise NotImplementedError + + async def subprocess_shell( + self, + protocol_factory, + cmd, + *, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + **kwargs, + ): + raise NotImplementedError + + async def subprocess_exec( + self, + protocol_factory, + *args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + **kwargs, + ): + raise NotImplementedError + + def add_reader(self, fd, callback, *args): + raise NotImplementedError + + def remove_reader(self, fd): + raise NotImplementedError + + def add_writer(self, fd, callback, *args): + raise NotImplementedError + + def remove_writer(self, fd): + raise NotImplementedError + + async def sock_recv(self, sock, nbytes): + raise NotImplementedError + + async def sock_recv_into(self, sock, buf): + raise NotImplementedError + + async def sock_recvfrom(self, sock, bufsize): + raise NotImplementedError + + async def sock_recvfrom_into(self, sock, buf, nbytes=0): + raise NotImplementedError + + async def sock_sendall(self, sock, data): + raise NotImplementedError + + async def sock_sendto(self, sock, data, address): + raise NotImplementedError + + async def sock_connect(self, sock, address): + raise NotImplementedError + + async def sock_accept(self, sock): + raise NotImplementedError + + async def sock_sendfile(self, sock, file, offset=0, count=None, *, fallback=None): + raise NotImplementedError + + def add_signal_handler(self, sig, callback, *args): + raise NotImplementedError + + def remove_signal_handler(self, sig): + raise NotImplementedError + + def set_task_factory(self, factory): + raise NotImplementedError + + def get_task_factory(self): + raise NotImplementedError + + def get_exception_handler(self): + raise NotImplementedError + + def set_exception_handler(self, handler): + raise NotImplementedError + + def default_exception_handler(self, context): + raise NotImplementedError + + def set_debug(self, enabled): + raise NotImplementedError + + +async def register(loop: PollLoop, pollable: Pollable): + waker = loop.create_future() + loop.wakers.append((pollable, waker)) + await waker diff --git a/fastly_compute/testing/stubs/wit_world/__init__.py b/fastly_compute/testing/stubs/wit_world/__init__.py new file mode 100644 index 0000000..23f11ee --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/__init__.py @@ -0,0 +1,15 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +class WitWorld(Protocol): + pass diff --git a/fastly_compute/testing/stubs/wit_world/exports/__init__.py b/fastly_compute/testing/stubs/wit_world/exports/__init__.py new file mode 100644 index 0000000..37d64ef --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/exports/__init__.py @@ -0,0 +1,33 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import async_io +from ..imports import http_req + +class HttpIncoming(Protocol): + + @abstractmethod + def handle(self, request: http_req.Request, body: async_io.Pollable) -> None: + """ + Handle the given request. + + This function is called once per sandbox. When it returns, the + sandbox exits. To opt into receiving multiple requests in a single + sandbox, use `http-downstream.next-request`. + + To send a response for the given `request`, use `send-downstream`, or to + stream the response body after the response has been initiated, use + `send-downstream-streaming`. + + Raises: `wit_world.types.Err(None)` + """ + raise NotImplementedError + + diff --git a/fastly_compute/testing/stubs/wit_world/exports/http_incoming.py b/fastly_compute/testing/stubs/wit_world/exports/http_incoming.py new file mode 100644 index 0000000..cbb47ca --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/exports/http_incoming.py @@ -0,0 +1,21 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +The exported interface. + +The `handle` function serves as the main entrypoint to applications. Unlike the +rest of the interfaces in this package, this `http-incoming` interface is exported by +applications rather than imported, which means that this is a function defined +by the application and called from the outside, rather than a function called +by the application into the outside. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/__init__.py b/fastly_compute/testing/stubs/wit_world/imports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastly_compute/testing/stubs/wit_world/imports/acl.py b/fastly_compute/testing/stubs/wit_world/imports/acl.py new file mode 100644 index 0000000..acf1be0 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/acl.py @@ -0,0 +1,62 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +Blocklists using [Access Control Lists] (ACLs) + +[Access Control Lists]: https://www.fastly.com/documentation/reference/api/acls/ +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import types +from ..imports import async_io + +class AclError(Enum): + """ + Errors returned on ACL lookup failure. + """ + TOO_MANY_REQUESTS = 0 + GENERIC_ERROR = 1 + +class Acl: + """ + An ACL. + """ + + @classmethod + 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)` + """ + raise NotImplementedError + def lookup(self, ip_addr: types.IpAddress) -> Optional[async_io.Pollable]: + """ + Performs a lookup of the given IP address in the ACL. + + If any matches are found, the result is a JSON-encoded HTTP body. + + 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)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/async_io.py b/fastly_compute/testing/stubs/wit_world/imports/async_io.py new file mode 100644 index 0000000..26d6b5a --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/async_io.py @@ -0,0 +1,105 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +Async IO support. + +This module provides several utilities for performing I/O asynchronously. +See the documentation for `async-io.pollable` for a description of the kinds +of events it supports. + +In the future, this interface is expected to be replaced by +[integrated async features]. + +[integrated async features]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md#-async-explainer +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class Pollable: + """ + An object supporting generic async operations. + + Can be a `http-body.body`, `http-req.pending-response`, `http-req.pending-request`, + `cache.pending-entry`. `kv-store.pending-lookup`, `kv-store.pending-insert`, + `kv-store.pending-delete`, or `kv-store.pending-list`. + + Each async item has an associated I/O action: + + * Pending requests: awaiting the response headers / `response` object + * Normal bodies: reading bytes from the body + * Streaming bodies: writing bytes to the body + + For writing bytes, there is a large buffer associated with the handle that bytes + can eagerly be written into, even before the origin itself consumes that data. + """ + + def is_ready(self) -> bool: + """ + Make a nonblocking attempt to complete the I/O operation. + + Returns `true` if the given async item is “ready” for its associated I/O action, `false` + otherwise. + + If an object is ready, the I/O action is guaranteed to complete without blocking. + + Valid object handles includes bodies and pending requests. See the `async-io.pollable` + definition for more details, including what I/O actions are associated with each handle + type. + """ + raise NotImplementedError + @classmethod + def new_ready(cls) -> Self: + """ + Create a new trivial `pollable` which reports being immediately ready. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +def select(handles: List[Pollable]) -> int: + """ + Blocks until one of the given objects is ready for I/O. + + If an object is ready, the I/O action is guaranteed to complete without blocking. + + Valid object handles includes bodies and pending requests. See the `async-io.pollable` + definition for more details, including what I/O actions are associated with each handle + type. + + Returns the *index* (not handle!) of the first object that is ready. + + Traps if the list is empty. + """ + raise NotImplementedError +def select_with_timeout(handles: List[Pollable], timeout_ms: int) -> Optional[int]: + """ + Blocks until one of the given objects is ready for I/O, or the timeout expires. + + If an object is ready, the I/O action is guaranteed to complete without blocking. + + Valid object handles includes bodies and pending requests. See the `async-io.pollable` + definition for more details, including what I/O actions are associated with each handle + type. + + The timeout is specified in milliseconds. + + Returns the *index* (not handle!) of the first object that is ready, or `none` if the + timeout expires before any objects are ready for I/O. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/backend.py b/fastly_compute/testing/stubs/wit_world/imports/backend.py new file mode 100644 index 0000000..c8cd26c --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/backend.py @@ -0,0 +1,444 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Backends] API. + +A backend represents a service that the application can send requests to, potentially +caching the responses received. + +Backends come in one of two flavors: + * **Static Backends**: These backends are created using the Fastly UI or API, + and are predefined by the user. Static backends typically have short names that are + usable across every instance of a service. + * **Dynamic Backends**: These backends are created programmatically using the + `register-dynamic-backend` API. They are defined at runtime, and may or may not + be shared across sandboxes depending on how they are configured. + +To use a backend, pass it to a `send*` function. + +Future versions of this function may return an error if your service does not have a backend +with this name. + +[Backends]: https://www.fastly.com/documentation/guides/integrations/non-fastly-services/developer-guide-backends/ +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import secret_store + +class DynamicBackendOptions: + """ + Options for `register-dynamic-backend`. + """ + + def __init__(self) -> None: + """ + Constructs an options resource with default values for all other possible fields for the + backend, which can be overridden using the other methods provided. + """ + raise NotImplementedError + + def override_host(self, value: str) -> None: + """ + Sets a host header override when contacting this backend. + + This will force the value of the “Host” header to the given string when sending out the + origin request. If this is not set and no header already exists, the “Host” header will + default to the target. + + For more information, see [the Fastly documentation on override hosts]. + + [the Fastly documentation on override hosts]: https://docs.fastly.com/en/guides/specifying-an-override-host> + """ + raise NotImplementedError + def connect_timeout(self, value: int) -> None: + """ + Sets the connection timeout, in milliseconds, for this backend. + + Defaults to 1,000ms (1s). + """ + raise NotImplementedError + def first_byte_timeout(self, value: int) -> None: + """ + Sets a timeout, in milliseconds, that applies between the time of connection and the time we + get the first byte back. + + Defaults to 15,000ms (15s). + """ + raise NotImplementedError + def between_bytes_timeout(self, value: int) -> None: + """ + Sets a timeout, in milliseconds, that applies between any two bytes we receive across the + wire. + + Defaults to 10,000ms (10s). + """ + raise NotImplementedError + def use_tls(self, value: bool) -> None: + """ + Enables or disables TLS to connect to the backend. + + When using TLS, Fastly checks the validity of the backend’s certificate, and fails the + connection if the certificate is invalid. This check is not optional: an invalid + certificate will cause the backend connection to fail (but read on). + + By default, the validity check does not require that the certificate hostname matches the + hostname of your request. You can use `cert-hostname` to request a check of the + certificate hostname. + + By default, certificate validity uses a set of public certificate authorities. You can + specify an alternative CA using `ca-certificate`. + """ + raise NotImplementedError + def tls_min_version(self, value: int) -> None: + """ + Sets the minimum TLS version for connecting to the backend. + + Setting this will enable TLS for the connection as a side effect. + """ + raise NotImplementedError + def tls_max_version(self, value: int) -> None: + """ + Sets the maximum TLS version for connecting to the backend. + + Setting this will enable TLS for the connection as a side effect. ( + """ + raise NotImplementedError + def cert_hostname(self, value: str) -> None: + """ + Defines the hostname that the server certificate should declare, and turn on validation + during backend connections. + + You should enable this if you are using TLS, and setting this will enable TLS for the + connection as a side effect. + + If `cert-hostname` is not provided (default), the server certificate’s hostname may + have any value. + """ + raise NotImplementedError + def ca_certificate(self, value: str) -> None: + """ + Sets the CA certificate to use when checking the validity of the backend. + + Setting this will enable TLS for the connection as a side effect. + + If `ca-certificate` is not provided (default), the backends’s certificate is validated + using a set of public root CAs. + """ + raise NotImplementedError + def tls_ciphers(self, value: str) -> None: + """ + Sets the acceptable cipher suites to use for TLS 1.0 - 1.2 connections. + + Setting this will enable TLS for the connection as a side effect. + """ + raise NotImplementedError + def sni_hostname(self, value: str) -> None: + """ + Sets the SNI hostname for the backend connection. + + Setting this will enable TLS for the connection as a side effect. + """ + raise NotImplementedError + def client_cert(self, client_cert: str, key: secret_store.Secret) -> None: + """ + Provides the given client certificate to the server as part of the TLS handshake. + + Setting this will enable TLS for the connection as a side effect. Both the certificate and + the key to use should be in standard PEM format; providing the information in another + format will lead to an error. We suggest that (at least the) key should be held in + something like the Fastly secret store for security, with the handle passed to this + function without unpacking it via `secret.plaintext`; the certificate can be held in a less + secure medium. + + (If it is absolutely necessary to get the key from another source, we suggest the use of + `secret.from-bytes`. + """ + raise NotImplementedError + def http_keepalive_time_ms(self, value: int) -> None: + """ + Configures up to how long to allow HTTP keepalive connections to remain idle in the + connection pool. + """ + raise NotImplementedError + def tcp_keepalive_enable(self, value: int) -> None: + """ + Configures whether or not to use TCP keepalive on the connection to the backend. + """ + raise NotImplementedError + def tcp_keepalive_interval_secs(self, value: int) -> None: + """ + Configures how long to wait in between each TCP keepalive probe sent to the backend. + """ + raise NotImplementedError + def tcp_keepalive_probes(self, value: int) -> None: + """ + Configures up to how many TCP keepalive probes to send to the backend before the connection + is considered dead. + """ + raise NotImplementedError + def tcp_keepalive_time_secs(self, value: int) -> None: + """ + Configures how long to wait after the last sent data over the TCP connection before starting + to send TCP keepalive probes. + """ + raise NotImplementedError + def max_connections(self, value: int) -> None: + """ + Configures the maximum number of connections to keep in the local connection pool. + + `0` is unlimited. + + Note that this limit is best determined experimentally, since the total number of + connections to the backend will depend on POP sizes, HTTP keepalive limits, and the + traffic patterns for individual POPs. + """ + raise NotImplementedError + def max_use(self, value: int) -> None: + """ + Configures how many times a pooled connection can be used. + + `0` is unlimited. + """ + raise NotImplementedError + def max_lifetime_ms(self, value: int) -> None: + """ + Configures an upper bound for how long an HTTP keepalive connection can be open before we + stop trying to reuse it. + + `0` is unlimited. + """ + raise NotImplementedError + def pooling(self, value: bool) -> None: + """ + Determines whether or not connections to the same backend should be pooled across different + sandboxes. + + Fastly considers two backends “the same” if they’re registered with the same name and + the exact same settings. In those cases, when pooling is enabled, if one sandbox + opens a connection to this backend it will be left open, and can be re-used by a different + sandbox. This can help improve backend latency, by removing the need for the initial + network / TLS handshake(s). + + By default, pooling is enabled for dynamic backends. + """ + raise NotImplementedError + def grpc(self, value: bool) -> None: + """ + Sets whether or not this backend will be used for gRPC traffic. + + Warning: Setting this for backends that will not be used with gRPC may have unpredictable + effects. Fastly only currently guarantees that this connection will work for gRPC traffic. + """ + raise NotImplementedError + def prefer_ipv6(self, value: bool) -> None: + """ + Whether to prefer attempting connections to IPv6 addresses over IPv4 addresses when a + hostname has both A and AAAA records. + + The Compute platform defaults to `true`, and will attempt IPv6 first if a AAAA record + is present. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +class BackendHealth(Enum): + UNKNOWN = 0 + HEALTHY = 1 + UNHEALTHY = 2 + +class Backend: + + @classmethod + def open(cls, name: str) -> Self: + """ + Attempts to open the named static backend. + + Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)` + """ + raise NotImplementedError + def get_name(self) -> str: + """ + Returns the name of this backend. + """ + raise NotImplementedError + def is_healthy(self) -> BackendHealth: + """ + Returns the health of the backend if configured and currently known. + + For backends without a configured healthcheck, this will always return + `backend-health.unknown`. + + Raises: `wit_world.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)` + """ + 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)` + """ + raise NotImplementedError + def get_override_host(self, max_len: int) -> Optional[bytes]: + """ + Gets the “override host” for this backend. + + This is used to change the `Host` header sent to the backend. See + [the Fastly documentation on override hosts]. + + [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)` + """ + 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)` + """ + 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)` + """ + raise NotImplementedError + def get_first_byte_timeout_ms(self) -> int: + """ + Gets the first byte timeout of the backend. + + 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)` + """ + raise NotImplementedError + def get_between_bytes_timeout_ms(self) -> int: + """ + Gets the between byte timeout of the backend. + + This timeout applies between any two bytes we receive across the wire. + + Raises: `wit_world.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)` + """ + 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)` + """ + 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)` + """ + 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)` + """ + 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)` + """ + 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)` + """ + 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)` + """ + 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)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +def register_dynamic_backend(prefix: str, target: str, options: DynamicBackendOptions) -> Backend: + """ + Creates a new dynamic backend. + + The arguments are the name of the new backend to use, along with a string describing the + backend host. The latter can be of the form: + + - "" + - "" + - ":" + - ":" + + The name can be whatever you would like, as long as it does not match the name of any of the + static service backends nor match any other dynamic backends built during this service + instance. (Names can overlap between different instances of the same service—they will be + treated as completely separate entities and will not be pooled—but you cannot, for example, + declare a dynamic backend named “dynamic-backend” twice in the same sandbox.) + + Dynamic backends must be enabled for the Compute service. You can determine whether or not + dynamic backends have been allowed for the current service by checking for the + `error.unsupported` error result. This error only arises when attempting to use dynamic + 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)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/cache.py b/fastly_compute/testing/stubs/wit_world/imports/cache.py new file mode 100644 index 0000000..91bdf96 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/cache.py @@ -0,0 +1,485 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Core Cache] API + +[Core Cache]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/#core-cache +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import async_io +from ..imports import http_req + +class ExtraLookupOptions: + """ + Extensibility for `lookup-options` + """ + + def __init__(self) -> None: + raise NotImplementedError + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class LookupOptions: + """ + Options for cache lookup operations; currently used for both `lookup` and + `transaction-lookup`. + """ + request_headers: Optional[http_req.Request] + always_use_requested_range: bool + extra: Optional[ExtraLookupOptions] + +class ExtraWriteOptions: + """ + Extensibility for `write-options` + """ + + def __init__(self) -> None: + raise NotImplementedError + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class WriteOptions: + """ + Configuration for several functions that write to the cache: + - `insert` + - `transaction-insert` + - `transaction-insert-and-stream-back` + - `transaction-update` + + Some options are only allowed for certain of these hostcalls; see the comments + on the fields. + """ + max_age_ns: int + request_headers: Optional[http_req.Request] + vary_rule: Optional[str] + initial_age_ns: Optional[int] + stale_while_revalidate_ns: Optional[int] + surrogate_keys: Optional[str] + length: Optional[int] + user_metadata: Optional[bytes] + edge_max_age_ns: Optional[int] + sensitive_data: bool + extra: Optional[ExtraWriteOptions] + +class LookupState(Flag): + """ + The status of this lookup (and potential transaction) + """ + FOUND = auto() + USABLE = auto() + STALE = auto() + MUST_INSERT_OR_UPDATE = auto() + USABLE_IF_ERROR = auto() + +class ExtraGetBodyOptions: + """ + Extensibility for `get-body-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class GetBodyOptions: + from_: Optional[int] + to: Optional[int] + extra: Optional[ExtraGetBodyOptions] + +class Entry: + """ + The outcome of a cache lookup (either bare or as part of a cache transaction) + """ + + @classmethod + def lookup(cls, key: bytes, options: LookupOptions) -> Self: + """ + Performs a non-request-collapsing cache lookup. + + Returns a result without waiting for any request collapsing that may be ongoing. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + @classmethod + def transaction_lookup(cls, key: bytes, options: LookupOptions) -> Self: + """ + The entrypoint to the request-collapsing cache transaction API. + + This operation always participates in request collapsing and may return stale objects. To + bypass request collapsing, use `entry.lookup` or `insert` instead. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + @classmethod + def transaction_lookup_async(cls, key: bytes, options: LookupOptions) -> async_io.Pollable: + """ + The entrypoint to the request-collapsing cache transaction API, returning instead of waiting + on busy. + + This operation always participates in request collapsing and may return stale objects. To + bypass request collapsing, use `entry.lookup` or `insert` instead. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_insert(self, options: WriteOptions) -> async_io.Pollable: + """ + Inserts an object into the cache with the given metadata. + + Can only be used in if the cache handle state includes the `must-insert-or-update` flag. + + The returned handle is to a streaming body that is used for writing the object into + the cache. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_insert_and_stream_back(self, options: WriteOptions) -> Tuple[async_io.Pollable, Self]: + """ + Inserts an object into the cache with the given metadata, and return a readable stream of the + bytes as they are stored. + + This helps avoid the “slow reader” problem on a teed stream, for example when a program + wishes to store a backend request in the cache while simultaneously streaming to a client + in an HTTP response. + + The returned body handle is to a streaming body that is used for writing the object *into* + the cache. The returned cache handle provides a separate transaction for reading out the + newly cached object to send elsewhere. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_update(self, options: WriteOptions) -> None: + """ + Update the metadata of an object in the cache without changing its data. + + Can only be used in if the cache handle state includes both of the flags: + - `found` + - `must-insert-or-update` + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_state(self) -> LookupState: + """ + Get the state of a cache lookup, waiting for the lookup to complete if necessary. + + Note that FOUND == USABLE, and means "usable" (fresh or stale-while-revalidate). + Some SDKs were released that checked only FOUND to infer "usable"; + we preserve the equivalence for backwards compatibility. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_user_metadata(self, max_len: int) -> Optional[bytes]: + """ + Gets the user metadata of the found object, returning `ok(none)` if no object + was found. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_body(self, options: GetBodyOptions) -> async_io.Pollable: + """ + Gets a range of the found object body, returning `ok(none)` if there + was no found object. + + The returned `body` must be closed before calling this function again on the same + `entry`. + + Note: until the CacheD protocol is adjusted to fully support this functionality, + the body of objects that are past the stale-while-revalidate period will not + be available, even when other metadata is. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_length(self) -> Optional[int]: + """ + Gets the content length of the found object, returning `ok(none)` if + there was no found object, or no content length was provided. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_max_age_ns(self) -> Optional[int]: + """ + Gets the configured max age of the found object, returning `ok(none)` + if there was no found object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_stale_while_revalidate_ns(self) -> Optional[int]: + """ + Gets the configured stale-while-revalidate period of the found object, returning `ok(none)` + if there was no found object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_age_ns(self) -> Optional[int]: + """ + Gets the age of the found object, returning `ok(none)` if there + was no found object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_hits(self) -> Optional[int]: + """ + Gets the number of cache hits for the found object, returning `ok(none)` + if there was no found object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_cancel(self) -> None: + """ + Cancel an obligation to provide an object to the cache. + + Useful if there is an error before streaming is possible, for example if a backend is + unreachable. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +class ReplaceStrategy(Enum): + IMMEDIATE = 0 + IMMEDIATE_FORCE_MISS = 1 + WAIT = 2 + +class ExtraReplaceOptions: + """ + Extensibility for `replace-options` + """ + + def __init__(self) -> None: + raise NotImplementedError + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class ReplaceOptions: + """ + Options for cache replace operations + """ + request_headers: Optional[http_req.Request] + replace_strategy: Optional[ReplaceStrategy] + always_use_requested_range: bool + extra: Optional[ExtraReplaceOptions] + +class ReplaceEntry: + """ + A replace operation. + """ + + @classmethod + def replace(cls, key: bytes, options: ReplaceOptions) -> Self: + """ + The entrypoint to the replace API. + + This operation always participates in request collapsing and may return stale objects. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_age_ns(self) -> Optional[int]: + """ + Gets the age of the existing object during replace, returning + `ok(none)` if there was no object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_body(self, options: GetBodyOptions) -> Optional[async_io.Pollable]: + """ + Gets a range of the existing object body, returning `ok(none)` if there + was no existing object. + + The returned `body` must be closed before calling this function + again on the same `replace-entry`. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_hits(self) -> Optional[int]: + """ + Gets the number of cache hits for the existing object during replace, + returning `ok(none)` if there was no object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_length(self) -> Optional[int]: + """ + Gets the content length of the existing object during replace, + returning `ok(none)` if there was no object, or no content + length was provided. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_max_age_ns(self) -> Optional[int]: + """ + Gets the configured max age of the existing object during replace, + returning `ok(none)` if there was no object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_stale_while_revalidate_ns(self) -> Optional[int]: + """ + Gets the configured stale-while-revalidate period of the existing + object during replace, returning `ok(none)` if there was no + object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_state(self) -> Optional[LookupState]: + """ + Gets the lookup state of the existing object during replace, returning + `ok(none)` if there was no object. + + Note that FOUND == USABLE, and means "usable" (fresh or stale-while-revalidate). + Some SDKs were released that checked only FOUND to infer "usable"; + we preserve the equivalence for backwards compatibility. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_user_metadata(self, max_len: int) -> Optional[bytes]: + """ + Gets the user metadata of the existing object during replace, returning + `ok(none)` if there was no object. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +def insert(key: bytes, options: WriteOptions) -> async_io.Pollable: + """ + Performs a non-request-collapsing cache insertion (or update). + + The returned handle is to a streaming body that is used for writing the object into + the cache. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def await_entry(handle: async_io.Pollable) -> Entry: + """ + Continues the lookup transaction from which the given busy handle was returned, + waiting for the leader transaction if request collapsed, and returns a cache handle. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def close_pending_entry(handle: async_io.Pollable) -> None: + """ + Closes an interaction with the cache that has not yet finished request collapsing. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def close_entry(handle: Entry) -> None: + """ + Closes an ongoing interaction with the cache. + + If the cache handle state includes the `must-insert-or-update` (and hence no insert or + update has been performed), closing the handle cancels any request collapsing, potentially + choosing a new waiter to perform the insertion/update. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def replace_insert(handle: ReplaceEntry, options: WriteOptions) -> async_io.Pollable: + """ + Replace an object in the cache with the given metadata + + The returned handle is to a streaming body that is used for writing the object into + the cache. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def close_replace_entry(handle: ReplaceEntry) -> None: + """ + Closes an ongoing replace interaction with the cache. + + If the replace handle state includes the `must-insert-or-update` (and hence no insert or + update has been performed), closing the handle cancels any request collapsing, potentially + choosing a new waiter to perform the insertion/update. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/compute_runtime.py b/fastly_compute/testing/stubs/wit_world/imports/compute_runtime.py new file mode 100644 index 0000000..cb45cd3 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/compute_runtime.py @@ -0,0 +1,151 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +Features for interacting with the Compute runtime. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +def get_vcpu_ms() -> int: + """ + Gets the amount of vCPU time that has passed since this sandbox was started, in + milliseconds. + + This function returns only time spent running on a vCPU, and does not include time spent + performing any I/O operations. However, it is based on clock time passing, and so will include + time spent executing hostcalls, is heavily affected by what core of what CPU is running the + code, and can even be influenced by the state of the CPU. + + As a result, this function *should not be used in benchmarking across runs*. It can be used, + with caution, to compare the runtime of different operations within the same sandbox. + """ + raise NotImplementedError +def get_heap_mib() -> int: + """ + Get a snapshot of the current dynamic memory usage, rounded up to the nearest mebibyte (2^20). + + This includes usage from the Wasm linear memory (heap) and usage from host allocations + made on behalf of this sandbox, e.g. buffered bodies of HTTP responses. + The returned value is just a snapshot- it can change without any explicit action + by the sandbox (for instance, additional response data coming in from an HTTP response.) + It can also change over time / across runs, as the Compute platform's memory usage + changes. Consider the returned value with these uncertainties in mind. + """ + raise NotImplementedError +def get_sandbox_id() -> str: + """ + A UUID generated by Fastly for each sandbox. + + This is often a useful value to include in log messages, and also to send to upstream + servers as an additional custom HTTP header, allowing for straightforward correlation of + which sandbox processed a request to requests later processed by an origin server. + + By default, each sandbox handles exactly one downstream request, in which case + this sandbox UUID is unique for each request. However, by using + `http-downstream.next-request`, a single sandbox can accept multiple downstream + requests. For a UUID that reliably identifies a request, you may wish to use + `http-downstream.downstream-client-request-id`. + + Equivalent to the "FASTLY_TRACE_ID" environment variable. + """ + raise NotImplementedError +def get_hostname() -> str: + """ + The hostname of the Fastly cache server which is executing the current sandbox, for + example, `cache-jfk1034`. + + Equivalent to the "FASTLY_HOSTNAME" environment variable and to [`server.hostname`] in VCL. + + [`server.hostname`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-hostname/ + """ + raise NotImplementedError +def get_pop() -> str: + """ + The three-character identifying code of the [Fastly POP] in which the current service + instance is running. + + Equivalent to the "FASTLY_POP" environment variable and to [`server.datacenter`] in VCL. + + [Fastly POP]: https://www.fastly.com/documentation/guides/concepts/pop/ + [`server.datacenter`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-datacenter/ + """ + raise NotImplementedError +def get_region() -> str: + """ + A code representing the general geographic region in which the [Fastly POP] processing the + current Compute sandbox resides. + + Equivalent to the "FASTLY_REGION" environment variable and to [`server.region`] in VCL, and + has the same possible values. + + [`server.region`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-region/ + [Fastly POP]: https://www.fastly.com/documentation/guides/concepts/pop/ + """ + raise NotImplementedError +def get_cache_generation() -> int: + """ + The current cache generation value for this Fastly service. + + The cache generation value is incremented by [purge-all operations]. + + Equivalent to the "FASTLY_CACHE_GENERATION" environment variable and to + [`req.vcl.generation`] in VCL. + + [purge-all operations]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/ + [`req.vcl.generation`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-vcl-generation/ + """ + raise NotImplementedError +def get_customer_id() -> str: + """ + The customer ID of the Fastly customer account to which the currently executing Fastly + service belongs. + + Equivalent to the "FASTLY_CUSTOMER_ID" environment variable and to [`req.customer_id`] in VCL. + + [`req.customer_id`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-customer-id/ + """ + raise NotImplementedError +def get_is_staging() -> bool: + """ + Whether the request is running in the Fastly service's [staging environment]. + + `false` for production or `true` for staging. + + Equivalent to the "FASTLY_IS_STAGING" environment variable and to [`fastly.is_staging`] in VCL. + + [`fastly.is_staging`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/fastly-is-staging/ + [staging environment]: https://docs.fastly.com/products/staging + """ + raise NotImplementedError +def get_service_id() -> str: + """ + The identifier for the Fastly service that is processing the current request. + + Equivalent to the "FASTLY_SERVICE_ID" environment variable and to [`req.service_id`] in VCL. + + [`req.service_id`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-service-id/ + """ + raise NotImplementedError +def get_service_version() -> int: + """ + The version number for the Fastly service that is processing the current request. + + Equivalent to the "FASTLY_SERVICE_VERSION" environment variable and to [`req.vcl.version`] + in VCL. + + [`req.vcl.version`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-vcl-version/ + """ + raise NotImplementedError +def get_namespace_id() -> str: + """ + This function is not suitable for general-purpose use. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/config_store.py b/fastly_compute/testing/stubs/wit_world/imports/config_store.py new file mode 100644 index 0000000..3d0ca2f --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/config_store.py @@ -0,0 +1,51 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Config Store] API. + +[Config Store]: https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#config-stores +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class Store: + """ + A Config Store. + """ + + @classmethod + def open(cls, name: str) -> Self: + """ + Attempts to open the named config store. + + Names are case sensitive. + + Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)` + """ + raise NotImplementedError + def get(self, key: str, max_len: int) -> Optional[str]: + """ + Fetches a value from the config store, returning `ok(none)` if it doesn't exist. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/device_detection.py b/fastly_compute/testing/stubs/wit_world/imports/device_detection.py new file mode 100644 index 0000000..c9c4f38 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/device_detection.py @@ -0,0 +1,28 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +Device detection based on the User-Agent header. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +def lookup(user_agent: str, max_len: int) -> Optional[str]: + """ + Looks up the data associated with a particular User-Agent string. + + Returns a list of bytes containing JSON-encoded device data. See [here] for descriptions + of the JSON fields. + + [here]: https://www.fastly.com/documentation/reference/vcl/variables/client-request/client-identified/ + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/dictionary.py b/fastly_compute/testing/stubs/wit_world/imports/dictionary.py new file mode 100644 index 0000000..6c0ae2b --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/dictionary.py @@ -0,0 +1,54 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Compute Dictionaries] (deprecated in favor of `config-store`) + +[Compute Dictionaries]: https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#dictionaries +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class Dictionary: + """ + A Compute Dictionary. + """ + + @classmethod + def open(cls, name: str) -> Self: + """ + Opens a dictionary, given its name. + + Names are case sensitive. + + Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)` + """ + raise NotImplementedError + def lookup(self, key: str, max_len: int) -> Optional[str]: + """ + Tries to look up a value in this dictionary. + + If the lookup is successful, this function returns `ok(some(s))` containing the found + string `s`, or `ok(none)` if no entry with the given key was found. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/environment.py b/fastly_compute/testing/stubs/wit_world/imports/environment.py new file mode 100644 index 0000000..dc70f84 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/environment.py @@ -0,0 +1,36 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +def get_environment() -> List[Tuple[str, str]]: + """ + Get the POSIX-style environment variables. + + Each environment variable is provided as a pair of string variable names + and string value. + + Morally, these are a value import, but until value imports are available + in the component model, this import function should return the same + values each time it is called. + """ + raise NotImplementedError +def get_arguments() -> List[str]: + """ + Get the POSIX-style arguments to the program. + """ + raise NotImplementedError +def initial_cwd() -> Optional[str]: + """ + Return a path that programs should use as their initial current working + directory, interpreting `.` as shorthand for this. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/erl.py b/fastly_compute/testing/stubs/wit_world/imports/erl.py new file mode 100644 index 0000000..1c957d6 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/erl.py @@ -0,0 +1,128 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Edge rate limiting] API. + +[Edge rate limiting]: https://docs.fastly.com/products/edge-rate-limiting +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class PenaltyBox: + """ + A penaltybox that can be used with the edge rate limiter or + standalone for adding and checking if some entry is in the data set. + """ + + @classmethod + def open(cls, name: str) -> Self: + """ + Opens a `penalty-box` identified by the given name. + + Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)` + """ + raise NotImplementedError + def get_name(self) -> str: + """ + Returns the name of this penaltybox. + """ + raise NotImplementedError + def add(self, entry: str, ttl: int) -> None: + """ + Adds `entry` to a the penaltybox for the duration of ttl. + + Valid ttl span is 1m to 1h and TTL value is truncated to the nearest minute. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def has(self, entry: str) -> bool: + """ + Checks if `entry` is in the penaltybox. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +class RateCounter: + """ + A rate counter that can be used with a edge rate limiter or + standalone for counting and rate calculations. + """ + + @classmethod + def open(cls, name: str) -> Self: + """ + Opens a `rate-counter` with the given name. + + Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)` + """ + raise NotImplementedError + def get_name(self) -> str: + """ + Returns the name of this rate counter. + """ + raise NotImplementedError + def check_rate(self, entry: str, delta: int, window: int, limit: int, penalty_box: PenaltyBox, ttl: int) -> bool: + """ + Increments an entry in a rate counter and check if the client has exceeded some average number + of requests per second (RPS) over the window. + + If the client is over the rps limit for the window, add to the penaltybox for ttl. Valid ttl + span is 1m to 1h and TTL value is truncated to the nearest minute. + + Returns `true` if the client is penalized (i.e. should be limited), or `false` if not. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def increment(self, entry: str, delta: int) -> None: + """ + Increments an entry in the ratecounter by `delta`. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def lookup_rate(self, entry: str, window: int) -> int: + """ + Looks up the current rate for entry in the ratecounter for a window. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def lookup_count(self, entry: str, duration: int) -> int: + """ + Looks up the current count for entry in the ratecounter for duration. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/error.py b/fastly_compute/testing/stubs/wit_world/imports/error.py new file mode 100644 index 0000000..53d7969 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/error.py @@ -0,0 +1,56 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class Error: + """ + A resource which represents some error information. + + The only method provided by this resource is `to-debug-string`, + which provides some human-readable information about the error. + + In the `wasi:io` package, this resource is returned through the + `wasi:io/streams/stream-error` type. + + To provide more specific error information, other interfaces may + offer functions to "downcast" this error into more specific types. For example, + errors returned from streams derived from filesystem types can be described using + the filesystem's own error-code type. This is done using the function + `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + parameter and returns an `option`. + + The set of functions which can "downcast" an `error` into a more + concrete type is open. + """ + + def to_debug_string(self) -> str: + """ + Returns a string that is suitable to assist humans in debugging + this error. + + WARNING: The returned string should not be consumed mechanically! + It may change across platforms, hosts, or other implementation + details. Parsing this string is a major platform-compatibility + hazard. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/exit.py b/fastly_compute/testing/stubs/wit_world/imports/exit.py new file mode 100644 index 0000000..16c9f5e --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/exit.py @@ -0,0 +1,18 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +def exit(status: Result[None, None]) -> None: + """ + Exit the current instance and any linked instances. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/geo.py b/fastly_compute/testing/stubs/wit_world/imports/geo.py new file mode 100644 index 0000000..9660d34 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/geo.py @@ -0,0 +1,30 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Geographic data] for IP addresses. + +[Geographic data]: https://www.fastly.com/blog/improve-performance-and-gain-better-end-user-intelligence-geoip-geography-detection +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import types + + +def lookup(ip_addr: types.IpAddress, max_len: int) -> str: + """ + Looks up the geographic data associated with a particular IP address. + + Returns a list of bytes containing JSON-encoded geographic data. See [here] for descriptions + of the JSON fields. + + [here]: https://www.fastly.com/documentation/reference/vcl/variables/geolocation/ + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/http_body.py b/fastly_compute/testing/stubs/wit_world/imports/http_body.py new file mode 100644 index 0000000..c89fba5 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/http_body.py @@ -0,0 +1,155 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +HTTP bodies. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import types +from ..imports import async_io + + +@dataclass +class TrailerError_NotAvailableYet: + pass + + +@dataclass +class TrailerError_Error: + value: types.Error + + +TrailerError = Union[TrailerError_NotAvailableYet, TrailerError_Error] +""" +Trailers aren't available until the body has been completely transmitted, so this error +type can either indicate that the errors aren't available yet, or that an error occurred. +""" + + + +def new() -> async_io.Pollable: + """ + Creates a new empty body that can be used for outgoing requests and responses. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def append(dest: async_io.Pollable, src: async_io.Pollable) -> None: + """ + Appends the contents of the body `src` to the body `dest`. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def read(body: async_io.Pollable, chunk_size: int) -> bytes: + """ + Reads from a body. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def write(body: async_io.Pollable, buf: bytes) -> int: + """ + Writes to a body. + + This function may write fewer bytes than requested; on success, the number of + bytes actually written is returned. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def write_front(body: async_io.Pollable, buf: bytes) -> None: + """ + Prepends bytes to the front of a body. + + On success, this function always writes all the bytes of `buf`. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def close(body: async_io.Pollable) -> None: + """ + Frees a body. + + This releases resources associated with the body. + + For streaming bodies, this is a *successful* stream termination, which will signal + via framing that the body transfer is complete. + + If a handle is dropped without calling `close`, it's an *unsuccessful* stream + termination. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def get_known_length(body: async_io.Pollable) -> Optional[int]: + """ + Returns a `u64` body length if the length of a body is known, or `none` otherwise. + + If the length is unknown, it is likely due to the body arising from an HTTP/1.1 message with + chunked encoding, an HTTP/2 or later message with no `content-length`, or being a streaming + body. + + Receiving a length from this function does not guarantee that the full number of + bytes can actually be read from the body. For example, when proxying a response from a + backend, this length may reflect the `content-length` promised in the response, but if the + backend connection is closed prematurely, fewer bytes may be delivered before this body + handle can no longer be read. + """ + raise NotImplementedError +def append_trailer(body: async_io.Pollable, name: str, value: bytes) -> None: + """ + Adds a body trailing header with given value. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def get_trailer_names(body: async_io.Pollable, max_len: int, cursor: int) -> Tuple[str, Optional[int]]: + """ + Gets the names of the trailers associated with this body. + + The first `cursor` names are skipped. The remaining names are encoded successively with + a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + names don't fit, the returned `option` is the index of the first name that didn't fit, + or `none` if all the remaining names fit. If `max-len` is too small to fit any name, an + `error.buffer-len` error is returned, providing a recommended buffer size. + + Raises: `wit_world.types.Err(wit_world.imports.http_body.TrailerError)` + """ + raise NotImplementedError +def get_trailer_value(body: async_io.Pollable, name: str, max_len: int) -> Optional[bytes]: + """ + Gets the value for the trailer with the given name, or `none` if the trailer is not present. + + If there are multiple values for this header, only one is returned, which may be + any of the values. See `get-trailer-values` if you need to get all of the values. + + This functions returns `ok(some(v))` if the trailer with the given name is present, + and `ok(none)` if no trailer with the given name is present. If `max-len` is too + small to fit the value, an `error.buffer-len` error is returned, providing a + recommended buffer size. + + Raises: `wit_world.types.Err(wit_world.imports.http_body.TrailerError)` + """ + raise NotImplementedError +def get_trailer_values(body: async_io.Pollable, name: str, max_len: int, cursor: int) -> Tuple[bytes, Optional[int]]: + """ + Gets multiple values associated with the trailer with the given name. + + As opposed to `get-trailer-value`, this function returns all of the values for this trailer. + + The first `cursor` values are skipped. The remaining values are encoded successively with + a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + values don't fit, the returned `option` is the index of the first value that didn't + fit, or `none` if all the remaining values fit. If `max-len` is too small to fit any value, + an `error.buffer-len` error is returned, providing a recommended buffer size. + + Raises: `wit_world.types.Err(wit_world.imports.http_body.TrailerError)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/http_cache.py b/fastly_compute/testing/stubs/wit_world/imports/http_cache.py new file mode 100644 index 0000000..421732e --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/http_cache.py @@ -0,0 +1,448 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[HTTP Cache] API. + +Overall, this should look very familiar to users of the Core Cache API. The primary differences +are: + +- HTTP `request`s and `response`s are used rather than relying on the user to + encode headers, status codes, etc in `user-metadata`. + +- Convenience functions specific to HTTP semantics are provided, such as `is-request-cacheable`, + `get-suggested-backend-request`, `get-suggested-write-options`, and + `transaction-record-not-cacheable`. + +The HTTP-specific behavior of these functions is intended to support applications that match the +normative guidance in [RFC 9111]. For example, `is-request-cacheable` returns `false` for `POST` +requests. However, this answer along with those of many of these functions explicitly provide +*suggestions*; they do not necessarily need to be followed if custom behavior is required, such +as caching `POST` responses when the application author knows that to be safe. + +The starting points for this API are `lookup` (no request collapsing) and `transaction-lookup` +(request collapsing). + +[HTTP Cache]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/cache-freshness/ +[RFC 9111]: https://www.rfc-editor.org/rfc/rfc9111.html +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import async_io +from ..imports import cache +from ..imports import http_req +from ..imports import backend +from ..imports import http_resp + +class ExtraLookupOptions: + """ + Extensibility for `lookup-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class LookupOptions: + """ + Non-required options for cache lookups. + """ + override_key: Optional[bytes] + backend: Optional[backend.Backend] + extra: Optional[ExtraLookupOptions] + +class ExtraWriteOptions: + """ + Extensibility for `write-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class WriteOptions: + """ + Options for cache insertions and updates. + """ + max_age_ns: int + vary_rule: Optional[str] + initial_age_ns: Optional[int] + stale_while_revalidate_ns: Optional[int] + stale_if_error_ns: Optional[int] + surrogate_keys: Optional[str] + length: Optional[int] + sensitive_data: bool + extra: Optional[ExtraWriteOptions] + +class SuggestedWriteOptions: + """ + The methods in this resource return values that correspond to the fields in a + `write-options`. This type is used when a `write-options` value would + be returned, so that it can use `max-len` parameters when returning + dynamically-sized data, and so that it excludes the `extra` field, since borrowed + handles cannot be returned from functions. + """ + + def get_max_age_ns(self) -> int: + """ + Returns the suggested value for the `write-options.max-age-ns` field. + """ + raise NotImplementedError + def get_vary_rule(self, max_len: int) -> str: + """ + Returns the suggested value for the `write-options.vary-rule` field. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_initial_age_ns(self) -> int: + """ + Returns the suggested value for the `write-options.initial-age-ns` field. + """ + raise NotImplementedError + def get_stale_while_revalidate_ns(self) -> int: + """ + Returns the suggested value for the `write-options.stale-while-revalidate-ns` field. + """ + raise NotImplementedError + def get_surrogate_keys(self, max_len: int) -> str: + """ + Returns the suggested value for the `write-options.surrogate-keys` field. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_length(self) -> Optional[int]: + """ + Returns the suggested value for the `write-options.length` field. + """ + raise NotImplementedError + def get_sensitive_data(self) -> bool: + """ + Returns the suggested value for the `write-options.sensitive-data` field. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +class StorageAction(Enum): + """ + The suggested action to take for spec-recommended behavior following + `prepare-response-for-storage`. + """ + INSERT = 0 + UPDATE = 1 + DO_NOT_STORE = 2 + RECORD_UNCACHEABLE = 3 + +class Entry: + """ + An HTTP Cache transaction. + """ + + @classmethod + def transaction_lookup(cls, req_handle: http_req.Request, options: LookupOptions) -> Self: + """ + Performs a cache lookup based on the given request. + + This operation always participates in request collapsing and may return an obligation to + insert or update responses, and/or stale responses. + + The request is not consumed. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_insert(self, resp_handle: http_resp.Response, options: WriteOptions) -> async_io.Pollable: + """ + Inserts a response into the cache with the given options, returning a streaming body handle + that is ready for writing or appending. + + Can only be used if the cache handle state includes the `must-insert-or-update` flag. + + The response is consumed. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_insert_and_stream_back(self, resp_handle: http_resp.Response, options: WriteOptions) -> Tuple[async_io.Pollable, Self]: + """ + Inserts a response into the cache with the given options, and return a fresh cache handle + that can be used to retrieve and stream the response while it's being inserted. + + This helps avoid the “slow reader” problem on a teed stream, for example when a program + wishes to store a backend request in the cache while simultaneously streaming to a client + in an HTTP response. + + The response is consumed. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_update(self, resp_handle: http_resp.Response, options: WriteOptions) -> None: + """ + Updates freshness lifetime, response headers, and caching settings without updating the + response body. + + Can only be used in if the cache handle state includes both of the flags: + - `found` + - `must-insert-or-update` + + The response is consumed. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_update_and_return_fresh(self, resp_handle: http_resp.Response, options: WriteOptions) -> Self: + """ + Updates freshness lifetime, response headers, and caching settings without updating the + response body, and return a fresh cache handle that can be used to retrieve and stream the + stored response. + + Can only be used in if the cache handle state includes both of the flags: + - `found` + - `must-insert-or-update` + + The response is consumed. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_record_not_cacheable(self, options: WriteOptions) -> None: + """ + Disables request collapsing and response caching for this cache entry. + + In Varnish terms, this function stores a hit-for-pass object. + + Only the max age and, optionally, the vary rule are read from the `options` + for this function. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_suggested_backend_request(self) -> http_req.Request: + """ + Prepares a suggested request to make to a backend to satisfy the looked-up request. + + If there is a stored, stale response, this suggested request may be for revalidation. If the + looked-up request is ranged, the suggested request will be unranged in order to try caching + the entire response. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_suggested_write_options(self, response: http_resp.Response) -> SuggestedWriteOptions: + """ + Prepares a suggested set of cache write options for a given request and response pair. + + The response is not consumed. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def prepare_response_for_storage(self, response: http_resp.Response) -> Tuple[StorageAction, http_resp.Response]: + """ + Adjusts a response into the appropriate form for storage and provides a storage action + recommendation. + + For example, if the looked-up request contains conditional headers, this function will + interpret a `304 Not Modified` response for revalidation by updating headers. + + In addition to the updated response, this function returns the recommended storage action. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_found_response(self, transform_for_client: int) -> Optional[Tuple[http_resp.Response, async_io.Pollable]]: + """ + Retrieves a stored response from the cache, returning `ok(none)` if + there was no response found. + + If `transform-for-client` is set, the response will be adjusted according to the looked-up + request. For example, a response retrieved for a range request may be transformed into a + `206 Partial Content` response with an appropriate `content-range` header. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_state(self) -> cache.LookupState: + """ + Gets the state of a cache transaction. + + Primarily useful after performing the lookup to determine what subsequent operations are + possible and whether any insertion or update obligations exist. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_length(self) -> Optional[int]: + """ + Gets the length of the found response, returning `ok(none)` if there + was no response found or no length was provided. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_max_age_ns(self) -> Optional[int]: + """ + Gets the configured max age of the found response in nanoseconds, returning `ok(none)` + if there was no response found. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_stale_while_revalidate_ns(self) -> Optional[int]: + """ + Gets the configured stale-while-revalidate period of the found response in nanoseconds, + returning `ok(none)` if there was no response found. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_age_ns(self) -> Optional[int]: + """ + Gets the age of the found response in nanoseconds, returning `ok(none)` + if there was no response found. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_hits(self) -> Optional[int]: + """ + Gets the number of cache hits for the found response, returning `ok(none)` + if there was no response found. + + This figure only reflects hits for a stored response in a particular cache server + or cluster, not the entire Fastly network. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_sensitive_data(self) -> Optional[bool]: + """ + Gets whether a found response is marked as containing sensitive data, returning `ok(none)` + if there was no response found. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_surrogate_keys(self, max_len: int) -> Optional[str]: + """ + Gets the surrogate keys of the found response, returning `ok(none)` if + there was no response found. + + The output is a list of surrogate keys separated by spaces. + + If the full list requires more than `max-len` bytes, an `error.buffer-len` + error is returned containing the required size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_vary_rule(self, max_len: int) -> Optional[str]: + """ + Gets the vary rule of the found response, returning `ok(none)` if there + was no response found. + + The output is a list of header names separated by spaces. + + If the full list requires more than `max-len` bytes, an `error.buffer-len` + error is returned containing the required size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def transaction_abandon(self) -> None: + """ + Abandons an obligation to provide a response to the cache. + + Useful if there is an error before streaming is possible, for example if a backend is + unreachable. + + If there are other requests collapsed on this transaction, one of those other requests will + be awoken and given the obligation to provide a response. If subsequent requests + are unlikely to yield cacheable responses, this may lead to undesired serialization of + requests. Consider using `transaction-record-not-cacheable` to make lookups for this request + bypass the cache. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +def is_request_cacheable(request: http_req.Request) -> bool: + """ + Determines whether a request is cacheable per conservative [RFC 9111] semantics. + + In particular, this function checks whether the request method is `GET` or `HEAD`, and + considers requests with other methods uncacheable. Applications where it is safe to cache + responses to other methods should consider using their own cacheability check instead of + this function. + + [RFC 9111]: https://www.rfc-editor.org/rfc/rfc9111.html + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def get_suggested_cache_key(request: http_req.Request, max_len: int) -> bytes: + """ + Retrieves the default cache key for the request. + + If the full key requires more than `max-len` bytes, an `error.buffer-len` + error is returned containing the required size. + + At the moment, HTTP cache keys must always be 32 bytes. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def close_entry(handle: Entry) -> None: + """ + Closes an ongoing interaction with the cache. + + If the cache handle state includes `must-insert-or-update` (and hence no insert or update + has been performed), closing the handle cancels any request collapsing, potentially choosing + a new waiter to perform the insertion/update. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/http_downstream.py b/fastly_compute/testing/stubs/wit_world/imports/http_downstream.py new file mode 100644 index 0000000..193633f --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/http_downstream.py @@ -0,0 +1,239 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +HTTP downstream requests and metadata. + +“Downstream” here refers to incoming HTTP requests. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import types +from ..imports import http_req +from ..imports import async_io + +class ExtraNextRequestOptions: + """ + Extensibility for `next-request-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class NextRequestOptions: + """ + Configuration for `next-request`. + """ + timeout_ms: Optional[int] + extra: Optional[ExtraNextRequestOptions] + + +def next_request(options: NextRequestOptions) -> async_io.Pollable: + """ + Prepares to accept a new downstream request. + + By default, each sandbox accepts a single downstream request, + passed in as an argument to the `http-incoming.handle` call. The + `next-request` function enables a sandbox to accept additional + downstream requests. `next-request` returns a `pending-request`, which + can be passed to `await-request` to wait for the request to become + available and return the `request`. + + When using this function, be mindful of two considerations: + + - When a single sandbox accepts multiple requests, its state + isn't automatically cleared between requests. Applications are + therefore responsible for preventing sensitive data from leaking from + one request to another. + + - There is no guarantee that a single sandbox will receive all + requests from a given client or from a given location. When it is + necessary to preserve state between multiple requests, store it outside + of the sandbox. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def await_request(pending: async_io.Pollable) -> Optional[Tuple[http_req.Request, async_io.Pollable]]: + """ + Waits until the next request is available, and then returns the resulting + request and body. + + Returns `ok(none)` if there are no more requests for this sandbox. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_original_header_names(ds_request: http_req.Request, max_len: int, cursor: int) -> Tuple[str, Optional[int]]: + """ + Returns the client request's header names exactly as they were originally received. + + This includes both the original header name characters' cases, as well as the original order + of the received headers. + + The first `cursor` names are skipped. The remaining names are encoded successively with + a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + names don't fit, the returned `option` is the index of the first name that didn't fit, + or `none` if all the remaining names fit. If `max-len` is too small to fit any name, + an `error.buffer-len` error is returned, providing a recommended buffer size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_original_header_count(ds_request: http_req.Request) -> int: + """ + Returns the number of headers in the client request as originally received. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_client_ip_addr(ds_request: http_req.Request) -> Optional[types.IpAddress]: + """ + Returns the IP address of the client making the HTTP request, if known. + """ + raise NotImplementedError +def downstream_server_ip_addr(ds_request: http_req.Request) -> Optional[types.IpAddress]: + """ + Returns the IP address on which this server received the HTTP request, if known. + """ + raise NotImplementedError +def downstream_client_h2_fingerprint(ds_request: http_req.Request, max_len: int) -> str: + """ + Gets the HTTP/2 fingerprint of client request if available. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_client_request_id(ds_request: http_req.Request, max_len: int) -> str: + """ + Gets the id of the current request if available. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_client_oh_fingerprint(ds_request: http_req.Request, max_len: int) -> str: + """ + Gets the fingerprint of client request headers if available. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_client_ddos_detected(ds_request: http_req.Request) -> bool: + """ + Returns whether the request was tagged as contributing to a DDoS attack. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_tls_cipher_openssl_name(ds_request: http_req.Request, max_len: int) -> Optional[bytes]: + """ + Gets the cipher suite used to secure the downstream client TLS connection. + + The value returned will be consistent with the [OpenSSL name] for the cipher suite. + + Returns `ok(none)` if the downstream client connection is not a TLS connection. + + [OpenSSL name]: https://testssl.sh/openssl-iana.mapping.html + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_tls_protocol(ds_request: http_req.Request, max_len: int) -> Optional[bytes]: + """ + Gets the TLS protocol version used to secure the downstream client TLS connection. + + Returns `ok(none)` if the downstream client connection is not a TLS connection. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_tls_client_hello(ds_request: http_req.Request, max_len: int) -> Optional[bytes]: + """ + Gets the raw bytes sent by the client in the TLS ClientHello message. + + See [RFC 5246] for details. + + Returns `ok(none)` if the downstream client connection is not a TLS connection. + + [RFC 5246]: https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2 + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_tls_raw_client_certificate(ds_request: http_req.Request, max_len: int) -> Optional[bytes]: + """ + Gets the raw client certificate used to secure the downstream client mTLS connection. + + The value returned will be based on PEM format. + + Returns `ok(none)` if the downstream client connection is not a TLS connection. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_tls_client_cert_verify_result(ds_request: http_req.Request) -> Optional[http_req.ClientCertVerifyResult]: + """ + Returns the `client-cert-verify-result` from the downstream client mTLS handshake. + + Returns `ok(none)` if the downstream client connection is not a TLS connection. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_tls_client_servername(ds_request: http_req.Request, max_len: int) -> Optional[str]: + """ + Returns the Server Name Indication from the downstream client TLS handshake. + + Returns `ok(none)` if not available. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_tls_ja3_md5(ds_request: http_req.Request) -> Optional[bytes]: + """ + Gets the JA3 hash of the TLS ClientHello message. + + Returns `ok(none)` if the downstream client connection is not a TLS connection. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_tls_ja4(ds_request: http_req.Request, max_len: int) -> Optional[str]: + """ + Gets the JA4 hash of the TLS ClientHello message. + + Returns `ok(none)` if the downstream client connection is not a TLS connection. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def downstream_compliance_region(ds_request: http_req.Request, max_len: int) -> Optional[str]: + """ + Gets the compliance region that the client IP address is in. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def fastly_key_is_valid(ds_request: http_req.Request) -> bool: + """ + Returns whether or not the original client request arrived with a + Fastly-Key belonging to a user with the rights to purge content on this + service. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/http_req.py b/fastly_compute/testing/stubs/wit_world/imports/http_req.py new file mode 100644 index 0000000..d4b566e --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/http_req.py @@ -0,0 +1,565 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +HTTP requests. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import http_types +from ..imports import types +from ..imports import backend +from ..imports import async_io +from ..imports import http_resp + +class ExtraCacheOverrideDetails: + """ + Extensibility for `cache-override-details` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class CacheOverrideDetails: + """ + The fields for the `override` arm of `cache-override`. + + The origin response’s cache control headers will be used for ttl and + `stale-while-revalidate` if `none`. + """ + ttl: Optional[int] + stale_while_revalidate: Optional[int] + pci: bool + surrogate_key: Optional[str] + extra: Optional[ExtraCacheOverrideDetails] + + +@dataclass +class CacheOverride_None_: + pass + + +@dataclass +class CacheOverride_Pass: + pass + + +@dataclass +class CacheOverride_Override: + value: CacheOverrideDetails + + +CacheOverride = Union[CacheOverride_None_, CacheOverride_Pass, CacheOverride_Override] +""" +Optional override for response caching behavior. +""" + + +class Request: + """ + An HTTP request. + """ + + @classmethod + def new(cls) -> Self: + """ + Creates a new `request` with no method, URL, or headers, and an empty body. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_cache_override(self, cache_override: CacheOverride) -> None: + """ + Sets the cache override behavior for this request. + + This setting will override any cache directive headers returned in response to this request. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_header_names(self, max_len: int, cursor: int) -> Tuple[str, Optional[int]]: + """ + Reads the request's header names via a buffer of the provided size. + + The first `cursor` names are skipped. The remaining names are encoded successively with + a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + names don't fit, the returned `option` is the index of the first name that didn't fit, + or `none` if all the remaining names fit. If `max-len` is too small to fit any name, + an `error.buffer-len` error is returned, providing a recommended buffer size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_header_value(self, name: str, max_len: int) -> Optional[bytes]: + """ + Gets the value of a header, or `none` if the header is not present. + + If there are multiple values for the header, only one is returned. See + `get-header-values` if you need to get all of the values. + + If header name requires more than `max-len` bytes, this will return an `error.buffer-len` + containing the required size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_header_values(self, name: str, max_len: int, cursor: int) -> Tuple[bytes, Optional[int]]: + """ + Gets multiple header values for the given `name` via a buffer of the provided size. + + As opposed to `get-header-value`, this function returns all of the values for this header. + + The first `cursor` values are skipped. The remaining values are encoded successively with + a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + values don't fit, the returned `option` is the index of the first value that didn't + fit, or `none` if all the remaining values fit. If `max-len` is too small to fit any value, + an `error.buffer-len` error is returned, providing a recommended buffer size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_header_values(self, name: str, values: bytes) -> None: + """ + Sets the values for the given header name, replacing any headers that previously existed for + that name. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def insert_header(self, name: str, value: bytes) -> None: + """ + Sets a request header to the given value, discarding any previous values for the given + header name. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def append_header(self, name: str, value: bytes) -> None: + """ + Adds a request header with given value. + + Unlike `set-header-values`, this does not discard existing values for the same header name. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def remove_header(self, name: str) -> None: + """ + Removes all request headers of the given name + + Returns `ok` if any headers were successfully removed. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_method(self, max_len: int) -> str: + """ + Gets the request method. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_method(self, method: str) -> None: + """ + Sets the request method. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_uri(self, max_len: int) -> str: + """ + Gets the request URI. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_uri(self, uri: str) -> None: + """ + Sets the request URI. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_version(self) -> http_types.HttpVersion: + """ + Gets the HTTP version of this request. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_version(self, version: http_types.HttpVersion) -> None: + """ + Sets the HTTP version of this request. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_auto_decompress_response(self, encodings: http_types.ContentEncodings) -> None: + """ + Sets the content encodings to automatically decompress responses to this request. + + If the response to this request is encoded by one of the encodings set by this method, the + response will be presented to the Compute program in decompressed form with the + `Content-Encoding` and `Content-Length` headers removed. + + Not all of the flags defined in `content-encodings` are supported. Currently the only + supported flag is `content-encodings.gzip`. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def redirect_to_websocket_proxy(self, backend: backend.Backend) -> None: + """ + Passes the WebSocket directly to a backend. + + This can only be used on services that have the WebSockets feature enabled and on requests + that are valid WebSocket requests. + + The sending completes in the background. Once this method has been called, no other response + can be sent to this request, and the application can exit without affecting the send. + + See the [WebSockets passthrough] documentation for a high-level description of this feature. + + [WebSockets passthrough]: https://www.fastly.com/documentation/guides/concepts/real-time-messaging/websockets-tunnel/ + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_framing_headers_mode(self, mode: http_types.FramingHeadersMode) -> None: + """ + Sets how the framing headers `Content-Length` and `Transfer-Encoding` will be determined + when sending this request. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def redirect_to_grip_proxy(self, backend: backend.Backend) -> None: + """ + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +class ClientCertVerifyResult(Enum): + """ + TLS client certificate verified result from downstream. + """ + OK = 0 + BAD_CERTIFICATE = 1 + CERTIFICATE_REVOKED = 2 + CERTIFICATE_EXPIRED = 3 + UNKNOWN_CA = 4 + CERTIFICATE_MISSING = 5 + CERTIFICATE_UNKNOWN = 6 + +@dataclass +class DnsErrorDetail: + """ + Variant fields for `send-error.dns-error`. + """ + rcode: Optional[int] + info_code: Optional[int] + +@dataclass +class TlsAlertReceivedDetail: + """ + Variant fields for `send-error.tls-alert-received`. + """ + id: Optional[int] + +@dataclass +class H2ErrorDetail: + """ + Variant fields for `send-error.h2-error`. + """ + frame_type: int + error_code: int + +class ExtraSendErrorDetail: + """ + Extensibility for `send-error-detail` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +@dataclass +class SendErrorDetail_DnsTimeout: + pass + + +@dataclass +class SendErrorDetail_DnsError: + value: DnsErrorDetail + + +@dataclass +class SendErrorDetail_DestinationNotFound: + pass + + +@dataclass +class SendErrorDetail_DestinationUnavailable: + pass + + +@dataclass +class SendErrorDetail_DestinationIpUnroutable: + pass + + +@dataclass +class SendErrorDetail_ConnectionRefused: + pass + + +@dataclass +class SendErrorDetail_ConnectionTerminated: + pass + + +@dataclass +class SendErrorDetail_ConnectionTimeout: + pass + + +@dataclass +class SendErrorDetail_ConnectionLimitReached: + pass + + +@dataclass +class SendErrorDetail_TlsCertificateError: + pass + + +@dataclass +class SendErrorDetail_TlsConfigurationError: + pass + + +@dataclass +class SendErrorDetail_HttpIncompleteResponse: + pass + + +@dataclass +class SendErrorDetail_HttpResponseHeaderSectionTooLarge: + pass + + +@dataclass +class SendErrorDetail_HttpResponseBodyTooLarge: + pass + + +@dataclass +class SendErrorDetail_HttpResponseTimeout: + pass + + +@dataclass +class SendErrorDetail_HttpResponseStatusInvalid: + pass + + +@dataclass +class SendErrorDetail_HttpUpgradeFailed: + pass + + +@dataclass +class SendErrorDetail_HttpProtocolError: + pass + + +@dataclass +class SendErrorDetail_HttpRequestCacheKeyInvalid: + pass + + +@dataclass +class SendErrorDetail_HttpRequestUriInvalid: + pass + + +@dataclass +class SendErrorDetail_InternalError: + pass + + +@dataclass +class SendErrorDetail_TlsAlertReceived: + value: TlsAlertReceivedDetail + + +@dataclass +class SendErrorDetail_TlsProtocolError: + pass + + +@dataclass +class SendErrorDetail_H2Error: + value: H2ErrorDetail + + +@dataclass +class SendErrorDetail_Extra: + value: ExtraSendErrorDetail + + +SendErrorDetail = Union[SendErrorDetail_DnsTimeout, SendErrorDetail_DnsError, SendErrorDetail_DestinationNotFound, SendErrorDetail_DestinationUnavailable, SendErrorDetail_DestinationIpUnroutable, SendErrorDetail_ConnectionRefused, SendErrorDetail_ConnectionTerminated, SendErrorDetail_ConnectionTimeout, SendErrorDetail_ConnectionLimitReached, SendErrorDetail_TlsCertificateError, SendErrorDetail_TlsConfigurationError, SendErrorDetail_HttpIncompleteResponse, SendErrorDetail_HttpResponseHeaderSectionTooLarge, SendErrorDetail_HttpResponseBodyTooLarge, SendErrorDetail_HttpResponseTimeout, SendErrorDetail_HttpResponseStatusInvalid, SendErrorDetail_HttpUpgradeFailed, SendErrorDetail_HttpProtocolError, SendErrorDetail_HttpRequestCacheKeyInvalid, SendErrorDetail_HttpRequestUriInvalid, SendErrorDetail_InternalError, SendErrorDetail_TlsAlertReceived, SendErrorDetail_TlsProtocolError, SendErrorDetail_H2Error, SendErrorDetail_Extra] +""" +Information about errors encountered by sent requests. +""" + + +@dataclass +class ErrorWithDetail: + """ + An `error` code, optionally with extra request error information. + """ + detail: Optional[SendErrorDetail] + error: types.Error + + +def send(request: Request, body: async_io.Pollable, backend: backend.Backend) -> Tuple[http_resp.Response, async_io.Pollable]: + """ + Retrieves a response for the request, either from cache or by sending it + to the given backend server. + + Returns once the response headers have been received, or an error occurs. + + Raises: `wit_world.types.Err(wit_world.imports.http_req.ErrorWithDetail)` + """ + raise NotImplementedError +def send_uncached(request: Request, body: async_io.Pollable, backend: backend.Backend) -> Tuple[http_resp.Response, async_io.Pollable]: + """ + Sends the request directly to the backend server without performing any + caching or inserting any cache-related headers in the response. + + Returns once the response headers have been received, or an error occurs. + + Raises: `wit_world.types.Err(wit_world.imports.http_req.ErrorWithDetail)` + """ + raise NotImplementedError +def send_async(request: Request, body: async_io.Pollable, backend: backend.Backend) -> async_io.Pollable: + """ + Begins sending the request to the given backend server, and returns a + `pending-response` that can yield the backend response or an error. + + This method returns as soon as the request begins sending to the backend, + and transmission of the request body and headers will continue in the + background. + + This method allows for sending more than one request at once and receiving + their responses in arbitrary orders. See `pending-response` for more + details on how to wait on, poll, or select between pending responses. + + This method is also useful for sending requests where the response is + unimportant, but the request may take longer than the Compute program is + able to run, as the request will continue sending even after the program + that initiated it exits. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def send_async_uncached(request: Request, body: async_io.Pollable, backend: backend.Backend) -> async_io.Pollable: + """ + This is to `send-async` as `send-uncached` is to `send`. + + As with `send-uncached`, this function sends the request directly to the + backend server without performing any caching or inserting any + cache-related headers in the response. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def send_async_streaming(request: Request, body: async_io.Pollable, backend: backend.Backend) -> async_io.Pollable: + """ + Begins sending the request to the given backend server, and returns a + `pending-response` that can yield the backend response or an error. + + The `body` argument is not consumed, so that it can accept further data to send. + + The backend connection is only closed once `http-body.close` is called. The + `pending-response` will not yield a `response` until the body is finished. + + This method is most useful for programs that do some sort of processing or + inspection of a potentially-large client request body. Streaming allows the + program to operate on small parts of the body rather than having to read it all + into memory at once. + + This method returns as soon as the request begins sending to the backend, + and transmission of the request body and headers will continue in the + background. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def send_async_uncached_streaming(request: Request, body: async_io.Pollable, backend: backend.Backend) -> async_io.Pollable: + """ + This is to `send-async-streaming` as `send-uncached` is to `send`. + + As with `send-uncached`, this function sends the request directly to the + backend server without performing any caching or inserting any + cache-related headers in the response. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def await_response(pending: async_io.Pollable) -> Tuple[http_resp.Response, async_io.Pollable]: + """ + Waits until the request is completed, and then returns the resulting + response and body. + + Raises: `wit_world.types.Err(wit_world.imports.http_req.ErrorWithDetail)` + """ + raise NotImplementedError +def close(request: Request) -> None: + """ + Closes the `request`, releasing any associated resources. + + A `request` is automatically consumed when you send a request. You should call `close` + only if you have a `request` you don't intend to use anymore. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def upgrade_websocket(backend: backend.Backend) -> None: + """ + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/http_resp.py b/fastly_compute/testing/stubs/wit_world/imports/http_resp.py new file mode 100644 index 0000000..a295a8f --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/http_resp.py @@ -0,0 +1,209 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +HTTP responses. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import http_types +from ..imports import types +from ..imports import async_io + +class KeepaliveMode(Enum): + AUTOMATIC = 0 + NO_KEEPALIVE = 1 + +class Response: + """ + An HTTP response. + """ + + @classmethod + def new(cls) -> Self: + """ + Create a new `response`. + + The new `response` is created with status code 200 OK, no headers, and an empty body. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_header_names(self, max_len: int, cursor: int) -> Tuple[str, Optional[int]]: + """ + Read the response's header names via a buffer of the provided size. + + The first `cursor` names are skipped. The remaining names are encoded successively with + a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + names don't fit, the returned `option` is the index of the first name that didn't fit, + or `none` if all the remaining names fit. If `max-len` is too small to fit any name, + an `error.buffer-len` error is returned, providing a recommended buffer size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_header_value(self, name: str, max_len: int) -> Optional[bytes]: + """ + Gets the value of a header, or `none` if the header is not present. + + If there are multiple values for the header, only one is returned. See + `get-header-values` if you need to get all of the values. + + If header name requires more than `max-len` bytes, this will return an `error.buffer-len` + containing the required size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_header_values(self, name: str, max_len: int, cursor: int) -> Tuple[bytes, Optional[int]]: + """ + Gets multiple header values for the given `name` via a buffer of the provided size. + + As opposed to `get-header-value`, this function returns all of the values for this header. + + The first `cursor` values are skipped. The remaining values are encoded successively with + a NUL byte after each into a list of bytes at most `max-len` long. If any of the remaining + values don't fit, the returned `option` is the index of the first value that didn't + fit, or `none` if all the remaining values fit. If `max-len` is too small to fit any value, + an `error.buffer-len` error is returned, providing a recommended buffer size. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_header_values(self, name: str, values: bytes) -> None: + """ + Sets the values for the given header name, replacing any headers that previously existed for + that name. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def insert_header(self, name: str, value: bytes) -> None: + """ + Sets a response header to the given value, discarding any previous values for the given + header name. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def append_header(self, name: str, value: bytes) -> None: + """ + Add a response header with given value. + + Unlike `set-header-values`, this does not discard existing values for the same header name. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def remove_header(self, name: str) -> None: + """ + Remove all response headers of the given name + + Returns `ok` if any headers were successfully removed. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_version(self) -> http_types.HttpVersion: + """ + Gets the HTTP version of this response. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_version(self, version: http_types.HttpVersion) -> None: + """ + Sets the HTTP version of this response. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_status(self) -> int: + """ + Gets the HTTP status code of the response. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_status(self, status: int) -> None: + """ + Sets the HTTP status code of the response. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_framing_headers_mode(self, mode: http_types.FramingHeadersMode) -> None: + """ + Sets how the framing headers `Content-Length` and `Transfer-Encoding` will be determined + when sending this response. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def set_http_keepalive_mode(self, mode: KeepaliveMode) -> None: + """ + Adjust the response's connection reuse mode. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def get_remote_ip_addr(self) -> Optional[types.IpAddress]: + """ + Gets the destination IP address used for this response, if known. + """ + raise NotImplementedError + def get_remote_port(self) -> Optional[int]: + """ + Gets the destination port used for this response, if known. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +def send_downstream(response: Response, body: async_io.Pollable) -> None: + """ + Sends a response to the client that made the request passed to `http-incoming.handle`. + + This method returns as soon as the response header begins sending to the client, and + transmission of the response will continue in the background. + + Data for the body must be written before calling this function. To start a response + and write data to it afterwards, use `send-downstream-streaming` instead. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def send_downstream_streaming(response: Response, body: async_io.Pollable) -> None: + """ + Starts a response to the client that made the request passed to `http-incoming.handle`. + + The body is left open, allowing data to be written after calling this function. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def close(response: Response) -> None: + """ + Closes the `response`, releasing any associated resources. + + A `response` is consumed when you send a response to a client or stream one to a + client. You should call `close` only if you have a `response` you don't intend + to use anymore. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/http_types.py b/fastly_compute/testing/stubs/wit_world/imports/http_types.py new file mode 100644 index 0000000..a2a7535 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/http_types.py @@ -0,0 +1,55 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +Types used by HTTP interfaces in this package. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class HttpVersion(Enum): + """ + HTTP protocol versions. + """ + HTTP09 = 0 + HTTP10 = 1 + HTTP11 = 2 + H2 = 3 + H3 = 4 + +class ContentEncodings(Flag): + """ + HTTP [content encoding] flags + + The names of these flags correspond to + [IANA HTTP Content Coding Registry] names. + + Not all of these content encoding flags are supported in all APIs that + use this `content-encodings` type. See the documentation for individual + functions for details. + + [content encoding]: https://www.rfc-editor.org/rfc/rfc9110.html#field.content-encoding + [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding + """ + GZIP = auto() + BR = auto() + ZSTD = auto() + DCB = auto() + DCZ = auto() + AES128GCM = auto() + +class FramingHeadersMode(Enum): + """ + Determines how the framing headers (`Content-Length`/`Transfer-Encoding`) are set for a + request or response. + """ + AUTOMATIC = 0 + MANUALLY_FROM_HEADERS = 1 + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/image_optimizer.py b/fastly_compute/testing/stubs/wit_world/imports/image_optimizer.py new file mode 100644 index 0000000..1218683 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/image_optimizer.py @@ -0,0 +1,47 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Image Optimizer] API. + +[Image Optimizer]: https://www.fastly.com/documentation/guides/full-site-delivery/image-optimization/about-fastly-image-optimizer/ +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import http_resp +from ..imports import backend +from ..imports import http_req +from ..imports import async_io + +class ExtraImageOptimizerTransformOptions: + """ + Extensibility for `image-optimizer-transform-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class ImageOptimizerTransformOptions: + sdk_claims_opts: Optional[str] + extra: Optional[ExtraImageOptimizerTransformOptions] + + +def transform_image_optimizer_request(origin_image_request: http_req.Request, origin_image_request_body: Optional[async_io.Pollable], origin_image_request_backend: backend.Backend, io_transform_options: ImageOptimizerTransformOptions) -> Tuple[http_resp.Response, async_io.Pollable]: + """ + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/insecure.py b/fastly_compute/testing/stubs/wit_world/imports/insecure.py new file mode 100644 index 0000000..a6ff586 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/insecure.py @@ -0,0 +1,39 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +The insecure interface for insecure pseudo-random numbers. + +It is intended to be portable at least between Unix-family platforms and +Windows. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +def get_insecure_random_bytes(len: int) -> bytes: + """ + Return `len` insecure pseudo-random bytes. + + This function is not cryptographically secure. Do not use it for + anything related to security. + + There are no requirements on the values of the returned bytes, however + implementations are encouraged to return evenly distributed values with + a long period. + """ + raise NotImplementedError +def get_insecure_random_u64() -> int: + """ + Return an insecure pseudo-random `u64` value. + + This function returns the same type of pseudo-random data as + `get-insecure-random-bytes`, represented as a `u64`. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/insecure_seed.py b/fastly_compute/testing/stubs/wit_world/imports/insecure_seed.py new file mode 100644 index 0000000..c0a3ef5 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/insecure_seed.py @@ -0,0 +1,40 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +The insecure-seed interface for seeding hash-map DoS resistance. + +It is intended to be portable at least between Unix-family platforms and +Windows. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +def insecure_seed() -> Tuple[int, int]: + """ + Return a 128-bit value that may contain a pseudo-random value. + + The returned value is not required to be computed from a CSPRNG, and may + even be entirely deterministic. Host implementations are encouraged to + provide pseudo-random values to any program exposed to + attacker-controlled content, to enable DoS protection built into many + languages' hash-map implementations. + + This function is intended to only be called once, by a source language + to initialize Denial Of Service (DoS) protection in its hash-map + implementation. + + # Expected future evolution + + This will likely be changed to a value import, to prevent it from being + called multiple times and potentially used for purposes other than DoS + protection. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/kv_store.py b/fastly_compute/testing/stubs/wit_world/imports/kv_store.py new file mode 100644 index 0000000..e9d3bd0 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/kv_store.py @@ -0,0 +1,349 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +Interface to Fastly's [Compute KV Store]. + +For a high-level introduction to this feature, see this [blog post]. + +[Compute KV Store]: https://www.fastly.com/documentation/guides/concepts/edge-state/data-stores/#kv-stores +[blog post]: https://www.fastly.com/blog/introducing-the-compute-edge-kv-store-global-persistent-storage-for-compute-functions +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import async_io + +class Entry: + """ + A response from a KV Store Lookup operation. + + This type holds the `body`, metadata, and generation of found key. + """ + + def take_body(self) -> Optional[async_io.Pollable]: + """ + Take and return the body from this `entry`, if it has one; otherwise return `none`. + + After calling this method, this entry will no longer have a body. + """ + raise NotImplementedError + def metadata(self, max_len: int) -> Optional[str]: + """ + Read the metadata of the KV Store item, if present. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def generation(self) -> int: + """ + Read the current generation of the KV Store item. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +class ExtraKvError: + """ + Extensibility for `kv-error` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +@dataclass +class KvError_BadRequest: + pass + + +@dataclass +class KvError_PreconditionFailed: + pass + + +@dataclass +class KvError_PayloadTooLarge: + pass + + +@dataclass +class KvError_InternalError: + pass + + +@dataclass +class KvError_TooManyRequests: + pass + + +@dataclass +class KvError_GenericError: + pass + + +@dataclass +class KvError_Extra: + value: ExtraKvError + + +KvError = Union[KvError_BadRequest, KvError_PreconditionFailed, KvError_PayloadTooLarge, KvError_InternalError, KvError_TooManyRequests, KvError_GenericError, KvError_Extra] +""" +A value indicating the status of a KV store operation. +""" + + +class InsertMode(Enum): + """ + Selects the behavior for an insert when the new key matches an existing key. + + A KV store maintains the property that its keys are unique from each other. If an insert + has a key that doesn't match any key already in the store, then the pair of the key and the + new value is inserted into the store. However, if the insert's key does match a key already + in the store, then no new key-value pair is inserted, and the insert's `insert-mode.mode` + determines what it does instead. + """ + OVERWRITE = 0 + ADD = 1 + APPEND = 2 + PREPEND = 3 + +class ExtraInsertOptions: + """ + Extensibility for `insert-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class InsertOptions: + """ + Options for configuring the behavior of the `insert` function. + """ + background_fetch: bool + if_generation_match: Optional[int] + metadata: Optional[str] + time_to_live_sec: Optional[int] + mode: InsertMode + extra: Optional[ExtraInsertOptions] + +class ListMode(Enum): + """ + Modes of KV Store list operations. + + This type serves to facilitate alternative methods of cache interactions with list operations. + """ + STRONG = 0 + EVENTUAL = 1 + +class ExtraListOptions: + """ + Extensibility for `list-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class ListOptions: + """ + Options for `list` and `list-async`. + """ + mode: ListMode + cursor: Optional[str] + limit: Optional[int] + prefix: Optional[str] + extra: Optional[ExtraListOptions] + +class Store: + """ + A KV Store. + """ + + @classmethod + def open(cls, name: str) -> Self: + """ + Opens the KV Store with the given name. + + Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)` + """ + raise NotImplementedError + def lookup(self, key: str) -> Optional[Entry]: + """ + Looks up a value in the KV Store. + + Returns `ok(some(v))` with the value `v` that was found, `ok(none)` if no value was + found, or `err(e)` indicating the error `e` occurred. + + This function waits until the operation completes. + + Raises: `wit_world.types.Err(wit_world.imports.kv_store.KvError)` + """ + raise NotImplementedError + def lookup_async(self, key: str) -> async_io.Pollable: + """ + Look up a value in the KV Store asynchronously. + + This function initiates an async lookup of a value in the KV Store. Use + `await-lookup` to finish the lookup. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def insert(self, key: str, body: async_io.Pollable, options: InsertOptions) -> None: + """ + Inserts a value into the KV Store. + + If the KV Store already contains a value for this key, the `mode` field + of the `options` argument specifies how the existing value is handled. + + This function waits until the operation completes. + + Raises: `wit_world.types.Err(wit_world.imports.kv_store.KvError)` + """ + raise NotImplementedError + def insert_async(self, key: str, body: async_io.Pollable, options: InsertOptions) -> async_io.Pollable: + """ + Insert a value into the KV Store asynchronously. + + If the KV Store already contains a value for this key, the `mode` field + of the `options` argument specifies how the existing value is handled. + + This function initiates an async insert of a value in the KV Store. Use + `await-insert` to finish the lookup. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def delete(self, key: str) -> bool: + """ + Deletes a value in the KV Store. + + Returns `ok(true)` if a value was successfully deleted, `ok(false)` if no value was + found, or `err(e)` indicating the error `e` occurred. + + This function waits until the operation completes. + + Raises: `wit_world.types.Err(wit_world.imports.kv_store.KvError)` + """ + raise NotImplementedError + def delete_async(self, key: str) -> async_io.Pollable: + """ + Delete of a value in the KV Store. + + This function initiates an async delete of a value in the KV Store. Use + `await-delete` to finish the lookup. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def list(self, options: ListOptions) -> async_io.Pollable: + """ + Lists keys in the KV Store. + + Returns `ok(b)` with the body `b` on success, or `err(e)` indicating the error `e` + occurred. + + This function waits until the operation completes. + + Raises: `wit_world.types.Err(wit_world.imports.kv_store.KvError)` + """ + raise NotImplementedError + def list_async(self, options: ListOptions) -> async_io.Pollable: + """ + List of keys in the KV Store. + + This function initiates an async list value in the KV Store. Use + `await-list` to finish the lookup. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +def await_lookup(handle: async_io.Pollable) -> Optional[Entry]: + """ + Wait on the async lookup of a value in the KV Store. + + Returns `ok(some(v))` with the value `v` that was found, `ok(none)` if no value was + found, or `err(e)` indicating the error `e` occurred. + + Raises: `wit_world.types.Err(wit_world.imports.kv_store.KvError)` + """ + raise NotImplementedError +def await_insert(handle: async_io.Pollable) -> None: + """ + Wait on the async insert of a value in the KV Store. + + Returns `ok` if the `insert` succeeded, or an error code on failure. + + Raises: `wit_world.types.Err(wit_world.imports.kv_store.KvError)` + """ + raise NotImplementedError +def await_delete(handle: async_io.Pollable) -> bool: + """ + Wait on the async delete of a value in the KV Store. + + Returns `ok(true)` if a value was successfully deleted, `ok(false)` if no value was + found, or `err(e)` indicating the error `e` occurred. + + Raises: `wit_world.types.Err(wit_world.imports.kv_store.KvError)` + """ + raise NotImplementedError +def await_list(handle: async_io.Pollable) -> async_io.Pollable: + """ + Wait on the async list of keys in the KV Store. + + Returns `ok(b)` with the JSON-encoded body `b` on success, or `err(e)` indicating + the error `e` occurred. + + Raises: `wit_world.types.Err(wit_world.imports.kv_store.KvError)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/log.py b/fastly_compute/testing/stubs/wit_world/imports/log.py new file mode 100644 index 0000000..1b326a4 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/log.py @@ -0,0 +1,59 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +Low-level interface to Fastly's [Real-Time Log Streaming] endpoints. + +[Real-Time Log Streaming]: https://docs.fastly.com/en/guides/about-fastlys-realtime-log-streaming-features +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class Endpoint: + """ + A logging endpoint. + """ + + @classmethod + def open(cls, name: str) -> Self: + """ + Tries to get an endpoint by name. + + Currently, the conditions on an endpoint name are: + - It must not be empty. + - It must not contain newlines (`\n`) or colons (`:`). + - It must not be `stdout` or `stderr`, which are reserved for debugging. + + Names are case sensitive. Calling `get-endpoint` with a name that doesn't correspond to any + logging endpoint available in your service will still return a usable endpoint, and writes + to that endpoint will succeed. Refer to your service dashboard to diagnose missing log + events. + + Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)` + """ + raise NotImplementedError + def write(self, msg: bytes) -> None: + """ + Writes a data to the given endpoint. + + Each call to `write` with a non-empty message produces a single log event. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/monotonic_clock.py b/fastly_compute/testing/stubs/wit_world/imports/monotonic_clock.py new file mode 100644 index 0000000..6223714 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/monotonic_clock.py @@ -0,0 +1,49 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +WASI Monotonic Clock is a clock API intended to let users measure elapsed +time. + +It is intended to be portable at least between Unix-family platforms and +Windows. + +A monotonic clock is a clock which has an unspecified initial value, and +successive reads of the clock will produce non-decreasing values. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import poll + + +def now() -> int: + """ + Read the current value of the clock. + + The clock is monotonic, therefore calling this function repeatedly will + produce a sequence of non-decreasing values. + """ + raise NotImplementedError +def resolution() -> int: + """ + Query the resolution of the clock. Returns the duration of time + corresponding to a clock tick. + """ + raise NotImplementedError +def subscribe_instant(when: int) -> poll.Pollable: + """ + Create a `pollable` which will resolve once the specified instant + has occurred. + """ + raise NotImplementedError +def subscribe_duration(when: int) -> poll.Pollable: + """ + Create a `pollable` that will resolve after the specified duration has + elapsed from the time this function is invoked. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/poll.py b/fastly_compute/testing/stubs/wit_world/imports/poll.py new file mode 100644 index 0000000..0e27c1b --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/poll.py @@ -0,0 +1,72 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +A poll API intended to let users wait for I/O events on multiple handles +at once. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class Pollable: + """ + `pollable` represents a single I/O event which may be ready, or not. + """ + + def ready(self) -> bool: + """ + Return the readiness of a pollable. This function never blocks. + + Returns `true` when the pollable is ready, and `false` otherwise. + """ + raise NotImplementedError + def block(self) -> None: + """ + `block` returns immediately if the pollable is ready, and otherwise + blocks until ready. + + This function is equivalent to calling `poll.poll` on a list + containing only this pollable. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +def poll(in_: List[Pollable]) -> List[int]: + """ + Poll for completion on a set of pollables. + + This function takes a list of pollables, which identify I/O sources of + interest, and waits until one or more of the events is ready for I/O. + + The result `list` contains one or more indices of handles in the + argument list that is ready for I/O. + + This function traps if either: + - the list is empty, or: + - the list contains more elements than can be indexed with a `u32` value. + + A timeout can be implemented by adding a pollable from the + wasi-clocks API to the list. + + This function does not return a `result`; polling in itself does not + do any I/O so it doesn't fail. If any of the I/O sources identified by + the pollables has an error, it is indicated by marking the source as + being ready for I/O. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/purge.py b/fastly_compute/testing/stubs/wit_world/imports/purge.py new file mode 100644 index 0000000..be6ab32 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/purge.py @@ -0,0 +1,63 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Cache Purging] API. + +[Cache Purging]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/ +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class ExtraPurgeOptions: + """ + Extensibility for `purge-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class PurgeOptions: + soft_purge: bool + extra: Optional[ExtraPurgeOptions] + + +def purge_surrogate_key(surrogate_keys: str, purge_options: PurgeOptions) -> None: + """ + Purge a surrogate key for the current service. + + A surrogate key can be a max of 1024 characters. + A surrogate key must contain only printable ASCII characters (those between `0x21` and `0x7E`, + inclusive). + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def purge_surrogate_key_verbose(surrogate_keys: str, purge_options: PurgeOptions, max_len: int) -> str: + """ + Purge a surrogate key for the current service, and return the purge id. + + This is similar to `purge-surrogate-key`, but on success, returns a + [JSON purge response] containing an ASCII alphanumeric string identifying + a purging. + + [JSON purge response]: https://developer.fastly.com/reference/api/purging/#purge-tag + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/random.py b/fastly_compute/testing/stubs/wit_world/imports/random.py new file mode 100644 index 0000000..8eb58cd --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/random.py @@ -0,0 +1,43 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +WASI Random is a random data API. + +It is intended to be portable at least between Unix-family platforms and +Windows. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +def get_random_bytes(len: int) -> bytes: + """ + Return `len` cryptographically-secure random or pseudo-random bytes. + + This function must produce data at least as cryptographically secure and + fast as an adequately seeded cryptographically-secure pseudo-random + number generator (CSPRNG). It must not block, from the perspective of + the calling program, under any circumstances, including on the first + request and on requests for numbers of bytes. The returned data must + always be unpredictable. + + This function must always return fresh data. Deterministic environments + must omit this function, rather than implementing it with deterministic + data. + """ + raise NotImplementedError +def get_random_u64() -> int: + """ + Return a cryptographically-secure random or pseudo-random `u64` value. + + This function returns the same type of data as `get-random-bytes`, + represented as a `u64`. + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/secret_store.py b/fastly_compute/testing/stubs/wit_world/imports/secret_store.py new file mode 100644 index 0000000..b8a8687 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/secret_store.py @@ -0,0 +1,95 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Secret Store] API. + +[Secret Store]: https://www.fastly.com/documentation/reference/api/services/resources/secret-store/ +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +class Secret: + """ + An individual secret. + """ + + @classmethod + def from_bytes(cls, bytes: bytes) -> Self: + """ + Creates a new “secret” from the given memory. + + This is *not* the suggested way to create `secret`s; instead, we suggest using `get`. + This secret will *NOT* be shared with other sandboxes. + + This method can be used for data that should be secret, but is being obtained by + some other means than the secret store. New “secrets” created this way use plaintext + only, and live in the sandbox's memory unencrypted for much longer than secrets + generated by `get`. They should thus only be used in situations in which an API requires + a `secret`, but you cannot (for whatever reason) use a `store` to store them. + + As the early note says, this `secret` will be local to the current sandbox, and + will not be shared with other instances of this service. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def plaintext(self, max_len: int) -> bytes: + """ + Returns the plaintext value of this secret. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +class Store: + """ + A Secret Store. + """ + + @classmethod + def open(cls, name: str) -> Self: + """ + Opens the Secret Store with the given name. + + Raises: `wit_world.types.Err(wit_world.imports.types.OpenError)` + """ + raise NotImplementedError + def get(self, key: str) -> Optional[Secret]: + """ + Tries to look up a Secret by name in this secret store. + + If successful, this method returns `ok(some(s))` containing the found secret `s` if the + secret is found, or `ok(none)` if the secret was not found. + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/security.py b/fastly_compute/testing/stubs/wit_world/imports/security.py new file mode 100644 index 0000000..06b4ccf --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/security.py @@ -0,0 +1,57 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Fastly Next-Gen WAF] API. + +[Fastly Next-Gen WAF]: https://docs.fastly.com/en/ngwaf/ +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import async_io +from ..imports import http_req +from ..imports import types + +class ExtraInspectOptions: + """ + Extensibility for `inspect-options` + """ + + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +@dataclass +class InspectOptions: + """ + Configuration for inspecting a `request` using Security. + """ + corp: Optional[str] + workspace: Optional[str] + override_client_ip: Optional[types.IpAddress] + extra: Optional[ExtraInspectOptions] + + +def inspect(request: http_req.Request, body: async_io.Pollable, options: InspectOptions, max_len: int) -> str: + """ + Inspects request HTTP traffic using the [NGWAF] lookaside service. + + Returns a JSON-encoded string. + + [NGWAF]: https://docs.fastly.com/en/ngwaf/ + + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/shielding.py b/fastly_compute/testing/stubs/wit_world/imports/shielding.py new file mode 100644 index 0000000..07694a0 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/shielding.py @@ -0,0 +1,51 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +[Shielding] API. + +[Shielding]: https://www.fastly.com/documentation/guides/concepts/shielding/ +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import backend + +class ShieldBackendOptions: + """ + Options for `backend-for-shield`. + """ + + def __init__(self) -> None: + raise NotImplementedError + + def set_cache_key(self, cache_key: str) -> None: + raise NotImplementedError + def set_first_byte_timeout(self, timeout_ms: int) -> None: + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + +def shield_info(name: str, max_len: int) -> str: + """ + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError +def backend_for_shield(name: str, options: Optional[ShieldBackendOptions]) -> backend.Backend: + """ + Raises: `wit_world.types.Err(wit_world.imports.types.Error)` + """ + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/stderr.py b/fastly_compute/testing/stubs/wit_world/imports/stderr.py new file mode 100644 index 0000000..ad8ad52 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/stderr.py @@ -0,0 +1,15 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import streams + + +def get_stderr() -> streams.OutputStream: + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/stdin.py b/fastly_compute/testing/stubs/wit_world/imports/stdin.py new file mode 100644 index 0000000..fbe2483 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/stdin.py @@ -0,0 +1,15 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import streams + + +def get_stdin() -> streams.InputStream: + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/stdout.py b/fastly_compute/testing/stubs/wit_world/imports/stdout.py new file mode 100644 index 0000000..5f52616 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/stdout.py @@ -0,0 +1,15 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import streams + + +def get_stdout() -> streams.OutputStream: + raise NotImplementedError diff --git a/fastly_compute/testing/stubs/wit_world/imports/streams.py b/fastly_compute/testing/stubs/wit_world/imports/streams.py new file mode 100644 index 0000000..fc14206 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/streams.py @@ -0,0 +1,330 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +WASI I/O is an I/O abstraction API which is currently focused on providing +stream types. + +In the future, the component model is expected to add built-in stream types; +when it does, they are expected to subsume this API. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some +from ..imports import error +from ..imports import poll + + +@dataclass +class StreamError_LastOperationFailed: + value: error.Error + + +@dataclass +class StreamError_Closed: + pass + + +StreamError = Union[StreamError_LastOperationFailed, StreamError_Closed] +""" +An error for input-stream and output-stream operations. +""" + + +class InputStream: + """ + An input bytestream. + + `input-stream`s are *non-blocking* to the extent practical on underlying + platforms. I/O operations always return promptly; if fewer bytes are + promptly available than requested, they return the number of bytes promptly + available, which could even be zero. To wait for data to be available, + use the `subscribe` function to obtain a `pollable` which can be polled + for using `wasi:io/poll`. + """ + + def read(self, len: int) -> bytes: + """ + Perform a non-blocking read from the stream. + + When the source of a `read` is binary data, the bytes from the source + are returned verbatim. When the source of a `read` is known to the + implementation to be text, bytes containing the UTF-8 encoding of the + text are returned. + + This function returns a list of bytes containing the read data, + when successful. The returned list will contain up to `len` bytes; + it may return fewer than requested, but not more. The list is + empty when no bytes are available for reading at this time. The + pollable given by `subscribe` will be ready when more bytes are + available. + + This function fails with a `stream-error` when the operation + encounters an error, giving `last-operation-failed`, or when the + stream is closed, giving `closed`. + + When the caller gives a `len` of 0, it represents a request to + read 0 bytes. If the stream is still open, this call should + succeed and return an empty list, or otherwise fail with `closed`. + + The `len` parameter is a `u64`, which could represent a list of u8 which + is not possible to allocate in wasm32, or not desirable to allocate as + as a return value by the callee. The callee may return a list of bytes + less than `len` in size while more bytes are available for reading. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def blocking_read(self, len: int) -> bytes: + """ + Read bytes from a stream, after blocking until at least one byte can + be read. Except for blocking, behavior is identical to `read`. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def skip(self, len: int) -> int: + """ + Skip bytes from a stream. Returns number of bytes skipped. + + Behaves identical to `read`, except instead of returning a list + of bytes, returns the number of bytes consumed from the stream. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def blocking_skip(self, len: int) -> int: + """ + Skip bytes from a stream, after blocking until at least one byte + can be skipped. Except for blocking behavior, identical to `skip`. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def subscribe(self) -> poll.Pollable: + """ + Create a `pollable` which will resolve once either the specified stream + has bytes available to read or the other end of the stream has been + closed. + The created `pollable` is a child resource of the `input-stream`. + Implementations may trap if the `input-stream` is dropped before + all derived `pollable`s created with this function are dropped. + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + +class OutputStream: + """ + An output bytestream. + + `output-stream`s are *non-blocking* to the extent practical on + underlying platforms. Except where specified otherwise, I/O operations also + always return promptly, after the number of bytes that can be written + promptly, which could even be zero. To wait for the stream to be ready to + accept data, the `subscribe` function to obtain a `pollable` which can be + polled for using `wasi:io/poll`. + + Dropping an `output-stream` while there's still an active write in + progress may result in the data being lost. Before dropping the stream, + be sure to fully flush your writes. + """ + + def check_write(self) -> int: + """ + Check readiness for writing. This function never blocks. + + Returns the number of bytes permitted for the next call to `write`, + or an error. Calling `write` with more bytes than this function has + permitted will trap. + + When this function returns 0 bytes, the `subscribe` pollable will + become ready when this function will report at least 1 byte, or an + error. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def write(self, contents: bytes) -> None: + """ + Perform a write. This function never blocks. + + When the destination of a `write` is binary data, the bytes from + `contents` are written verbatim. When the destination of a `write` is + known to the implementation to be text, the bytes of `contents` are + transcoded from UTF-8 into the encoding of the destination and then + written. + + Precondition: check-write gave permit of Ok(n) and contents has a + length of less than or equal to n. Otherwise, this function will trap. + + returns Err(closed) without writing if the stream has closed since + the last call to check-write provided a permit. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def blocking_write_and_flush(self, contents: bytes) -> None: + """ + Perform a write of up to 4096 bytes, and then flush the stream. Block + until all of these operations are complete, or an error occurs. + + This is a convenience wrapper around the use of `check-write`, + `subscribe`, `write`, and `flush`, and is implemented with the + following pseudo-code: + + ```text + let pollable = this.subscribe(); + while !contents.is_empty() { + // Wait for the stream to become writable + pollable.block(); + let Ok(n) = this.check-write(); // eliding error handling + let len = min(n, contents.len()); + let (chunk, rest) = contents.split_at(len); + this.write(chunk ); // eliding error handling + contents = rest; + } + this.flush(); + // Wait for completion of `flush` + pollable.block(); + // Check for any errors that arose during `flush` + let _ = this.check-write(); // eliding error handling + ``` + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def flush(self) -> None: + """ + Request to flush buffered output. This function never blocks. + + This tells the output-stream that the caller intends any buffered + output to be flushed. the output which is expected to be flushed + is all that has been passed to `write` prior to this call. + + Upon calling this function, the `output-stream` will not accept any + writes (`check-write` will return `ok(0)`) until the flush has + completed. The `subscribe` pollable will become ready when the + flush has completed and the stream can accept more writes. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def blocking_flush(self) -> None: + """ + Request to flush buffered output, and block until flush completes + and stream is ready for writing again. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def subscribe(self) -> poll.Pollable: + """ + Create a `pollable` which will resolve once the output-stream + is ready for more writing, or an error has occurred. When this + pollable is ready, `check-write` will return `ok(n)` with n>0, or an + error. + + If the stream is closed, this pollable is always ready immediately. + + The created `pollable` is a child resource of the `output-stream`. + Implementations may trap if the `output-stream` is dropped before + all derived `pollable`s created with this function are dropped. + """ + raise NotImplementedError + def write_zeroes(self, len: int) -> None: + """ + Write zeroes to a stream. + + This should be used precisely like `write` with the exact same + preconditions (must use check-write first), but instead of + passing a list of bytes, you simply pass the number of zero-bytes + that should be written. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def blocking_write_zeroes_and_flush(self, len: int) -> None: + """ + Perform a write of up to 4096 zeroes, and then flush the stream. + Block until all of these operations are complete, or an error + occurs. + + This is a convenience wrapper around the use of `check-write`, + `subscribe`, `write-zeroes`, and `flush`, and is implemented with + the following pseudo-code: + + ```text + let pollable = this.subscribe(); + while num_zeroes != 0 { + // Wait for the stream to become writable + pollable.block(); + let Ok(n) = this.check-write(); // eliding error handling + let len = min(n, num_zeroes); + this.write-zeroes(len); // eliding error handling + num_zeroes -= len; + } + this.flush(); + // Wait for completion of `flush` + pollable.block(); + // Check for any errors that arose during `flush` + let _ = this.check-write(); // eliding error handling + ``` + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def splice(self, src: InputStream, len: int) -> int: + """ + Read from one stream and write to another. + + The behavior of splice is equivalent to: + 1. calling `check-write` on the `output-stream` + 2. calling `read` on the `input-stream` with the smaller of the + `check-write` permitted length and the `len` provided to `splice` + 3. calling `write` on the `output-stream` with that read data. + + Any error reported by the call to `check-write`, `read`, or + `write` ends the splice and reports that error. + + This function returns the number of bytes transferred; it may be less + than `len`. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def blocking_splice(self, src: InputStream, len: int) -> int: + """ + Read from one stream and write to another, with blocking. + + This is similar to `splice`, except that it blocks until the + `output-stream` is ready for writing, and the `input-stream` + is ready for reading, before performing the `splice`. + + Raises: `wit_world.types.Err(wit_world.imports.streams.StreamError)` + """ + raise NotImplementedError + def __enter__(self) -> Self: + """Returns self""" + return self + + def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None: + """ + Release this resource. + """ + raise NotImplementedError + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/types.py b/fastly_compute/testing/stubs/wit_world/imports/types.py new file mode 100644 index 0000000..5596929 --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/types.py @@ -0,0 +1,116 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +Types used by many interfaces in this package. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + + +@dataclass +class Error_GenericError: + pass + + +@dataclass +class Error_InvalidArgument: + pass + + +@dataclass +class Error_AuxiliaryError: + pass + + +@dataclass +class Error_BufferLen: + value: int + + +@dataclass +class Error_Unsupported: + pass + + +@dataclass +class Error_HttpInvalid: + pass + + +@dataclass +class Error_HttpUser: + pass + + +@dataclass +class Error_HttpIncomplete: + pass + + +@dataclass +class Error_CannotRead: + pass + + +@dataclass +class Error_HttpHeadTooLarge: + pass + + +@dataclass +class Error_HttpInvalidStatus: + pass + + +@dataclass +class Error_LimitExceeded: + pass + + +Error = Union[Error_GenericError, Error_InvalidArgument, Error_AuxiliaryError, Error_BufferLen, Error_Unsupported, Error_HttpInvalid, Error_HttpUser, Error_HttpIncomplete, Error_CannotRead, Error_HttpHeadTooLarge, Error_HttpInvalidStatus, Error_LimitExceeded] +""" +A common error type used by many functions in this package. + +TODO: In the future this should be split up into more-specific error +enums so that it better documents which errors each function can actually +return and what they mean. +""" + + +class OpenError(Enum): + """ + An error returned by `open`-like functions. + """ + INVALID_SYNTAX = 0 + NAME_TOO_LONG = 1 + RESERVED = 2 + NOT_FOUND = 3 + UNSUPPORTED = 4 + LIMIT_EXCEEDED = 5 + GENERIC_ERROR = 6 + + +@dataclass +class IpAddress_Ipv4: + value: Tuple[int, int, int, int] + + +@dataclass +class IpAddress_Ipv6: + value: Tuple[int, int, int, int, int, int, int, int] + + +IpAddress = Union[IpAddress_Ipv4, IpAddress_Ipv6] +""" +IPv4 or IPv6 addresses. +""" + + + diff --git a/fastly_compute/testing/stubs/wit_world/imports/wall_clock.py b/fastly_compute/testing/stubs/wit_world/imports/wall_clock.py new file mode 100644 index 0000000..53b36fe --- /dev/null +++ b/fastly_compute/testing/stubs/wit_world/imports/wall_clock.py @@ -0,0 +1,61 @@ +# This file is automatically generated by componentize-py +# It is not intended for manual editing. +""" +WASI Wall Clock is a clock API intended to let users query the current +time. The name "wall" makes an analogy to a "clock on the wall", which +is not necessarily monotonic as it may be reset. + +It is intended to be portable at least between Unix-family platforms and +Windows. + +A wall clock is a clock which measures the date and time according to +some external reference. + +External references may be reset, so this clock is not necessarily +monotonic, making it unsuitable for measuring elapsed time. + +It is intended for reporting the current date and time for humans. +""" +from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self, Callable +from types import TracebackType +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from componentize_py_types import Result, Ok, Err, Some + + +@dataclass +class Datetime: + """ + A time and date in seconds plus nanoseconds. + """ + seconds: int + nanoseconds: int + + +def now() -> Datetime: + """ + Read the current value of the clock. + + This clock is not monotonic, therefore calling this function repeatedly + will not necessarily produce a sequence of non-decreasing values. + + The returned timestamps represent the number of seconds since + 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + also known as [Unix Time]. + + The nanoseconds field of the output is always less than 1000000000. + + [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + """ + raise NotImplementedError +def resolution() -> Datetime: + """ + Query the resolution of the clock. + + The nanoseconds field of the output is always less than 1000000000. + """ + raise NotImplementedError diff --git a/fastly_compute/tests/__init__.py b/fastly_compute/tests/__init__.py index 5b82554..a841850 100644 --- a/fastly_compute/tests/__init__.py +++ b/fastly_compute/tests/__init__.py @@ -11,6 +11,6 @@ try: from componentize_py_types import Err except ImportError: - sys.path.append(str(Path(__file__).parent.parent.parent / "stubs")) + sys.path.append(str(Path(__file__).parent.parent / "testing" / "stubs")) else: del Err diff --git a/pyproject.toml b/pyproject.toml index ac30aac..8c57099 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ addopts = [ [tool.ruff] target-version = "py314" line-length = 88 +exclude = ["fastly_compute/testing/stubs"] [tool.ruff.lint] select = [ @@ -100,7 +101,7 @@ py-modules = ["app"] [tool.pyrefly] python-version = "3.14" -search-path = ["stubs"] +search-path = ["fastly_compute/testing/stubs"] use-ignore-files = true # Type-check source code, tests, and examples project-includes = [ @@ -112,4 +113,5 @@ project-includes = [ project-excludes = [ # CLI wrapper imports from native extension that doesn't exist until built "fastly_compute/fastly_compute_py.py", + "fastly_compute/testing/stubs", ]