diff --git a/python-example/Pipfile b/python-example/Pipfile new file mode 100644 index 0000000..c398b0d --- /dev/null +++ b/python-example/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.10" diff --git a/python-example/README.md b/python-example/README.md new file mode 100644 index 0000000..641a704 --- /dev/null +++ b/python-example/README.md @@ -0,0 +1,3 @@ +# python example + +This example was adapted from [python-wasm-component](https://github.com/michelleN/python-wasm-component) repo. See repo for more information on how to build python component. diff --git a/python-example/__pycache__/app.cpython-311.pyc b/python-example/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..7c5da5f Binary files /dev/null and b/python-example/__pycache__/app.cpython-311.pyc differ diff --git a/python-example/app.py b/python-example/app.py new file mode 100644 index 0000000..afa43a7 --- /dev/null +++ b/python-example/app.py @@ -0,0 +1,25 @@ +from http_app import exports + +from http_app.imports.types import ( + IncomingRequest, + OutgoingResponse, + ResponseOutparam, + OutgoingBody, + Fields, + Ok, +) + + +class IncomingHandler(exports.IncomingHandler): + def handle(self, request: IncomingRequest, response_out: ResponseOutparam): + response = OutgoingResponse(200, Fields([("HELLO", b"WORLD")])) + + response_body = response.write() + + ResponseOutparam.set(response_out, Ok(response)) + + response_stream = response_body.write() + response_stream.blocking_write_and_flush(b"Hello from python!") + response_stream.drop() + + OutgoingBody.finish(response_body, None) diff --git a/python-example/componentize_py_runtime/__init__.py b/python-example/componentize_py_runtime/__init__.py new file mode 100644 index 0000000..2e077c0 --- /dev/null +++ b/python-example/componentize_py_runtime/__init__.py @@ -0,0 +1,5 @@ + +from typing import List, Any + +def call_import(index: int, args: List[Any], result_count: int) -> List[Any]: + raise NotImplementedError diff --git a/python-example/http_app/__init__.py b/python-example/http_app/__init__.py new file mode 100644 index 0000000..a67c61d --- /dev/null +++ b/python-example/http_app/__init__.py @@ -0,0 +1,13 @@ +from typing import TypeVar, Generic, Union, Optional, Union, Protocol, Tuple, List, Any +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from .types import Result, Ok, Err, Some +import componentize_py_runtime + + + +class HttpApp(Protocol): + pass diff --git a/python-example/http_app/exports/__init__.py b/python-example/http_app/exports/__init__.py new file mode 100644 index 0000000..d4fa4ea --- /dev/null +++ b/python-example/http_app/exports/__init__.py @@ -0,0 +1,16 @@ +from typing import TypeVar, Generic, Union, Optional, Union, Protocol, Tuple, List, Any +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from ..types import Result, Ok, Err, Some +from ..imports import types + +class IncomingHandler(Protocol): + + @abstractmethod + def handle(self, request: types.IncomingRequest, response_out: types.ResponseOutparam) -> None: + raise NotImplementedError + + diff --git a/python-example/http_app/exports/incoming_handler.py b/python-example/http_app/exports/incoming_handler.py new file mode 100644 index 0000000..4017f5e --- /dev/null +++ b/python-example/http_app/exports/incoming_handler.py @@ -0,0 +1,9 @@ +from typing import TypeVar, Generic, Union, Optional, Union, Protocol, Tuple, List, Any +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from ..types import Result, Ok, Err, Some + + diff --git a/python-example/http_app/imports/__init__.py b/python-example/http_app/imports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-example/http_app/imports/poll.py b/python-example/http_app/imports/poll.py new file mode 100644 index 0000000..93a7217 --- /dev/null +++ b/python-example/http_app/imports/poll.py @@ -0,0 +1,60 @@ +""" +A poll API intended to let users wait for I/O events on multiple handles +at once. +""" +from typing import TypeVar, Generic, Union, Optional, Union, Protocol, Tuple, List, Any +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from ..types import Result, Ok, Err, Some +import componentize_py_runtime + + +class Pollable: + """ + A "pollable" handle. + """ + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + + +def poll_list(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. + + If the list contains more elements than can be indexed with a `u32` + value, this function traps. + + 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 reaedy for I/O. + """ + result = componentize_py_runtime.call_import(1, [in_], 1) + return result[0] + +def poll_one(in_: Pollable) -> None: + """ + Poll for completion on a single pollable. + + This function is similar to `poll-list`, but operates on only a single + pollable. When it returns, the handle is ready for I/O. + """ + result = componentize_py_runtime.call_import(2, [in_], 0) + return + diff --git a/python-example/http_app/imports/streams.py b/python-example/http_app/imports/streams.py new file mode 100644 index 0000000..5c4db41 --- /dev/null +++ b/python-example/http_app/imports/streams.py @@ -0,0 +1,397 @@ +""" +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, Union, Protocol, Tuple, List, Any +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from ..types import Result, Ok, Err, Some +import componentize_py_runtime +from ..imports import poll + +class Error: + """ + Contextual error information about the last failure that happened on + a read, write, or flush from an `input-stream` or `output-stream`. + + This type is returned through the `stream-error` type whenever an + operation on a stream directly fails or an error is discovered + after-the-fact, for example when a write's failure shows up through a + later `flush` or `check-write`. + + Interfaces such as `wasi:filesystem/types` provide functionality to + further "downcast" this error into interface-specific error information. + """ + + def to_debug_string(self) -> str: + """ + Returns a string that's suitable to assist humans in debugging this + error. + + The returned string will change across platforms and hosts which + means that parsing it, for example, would be a + platform-compatibility hazard. + """ + result = componentize_py_runtime.call_import(6, [self], 1) + return result[0] + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + + +@dataclass +class StreamErrorLastOperationFailed: + value: Error + + +@dataclass +class StreamErrorClosed: + pass + + +# An error for input-stream and output-stream operations. +StreamError = Union[StreamErrorLastOperationFailed, StreamErrorClosed] + +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. + + This function returns a list of bytes containing the data that was + read, along with a `stream-status` which, indicates whether further + reads are expected to produce data. The returned list will contain up to + `len` bytes; it may return fewer than requested, but not more. An + empty list and `stream-status:open` indicates no more data is + available at this time, and that the pollable given by `subscribe` + will be ready when more data is available. + + Once a stream has reached the end, subsequent calls to `read` or + `skip` will always report `stream-status:ended` rather than producing more + data. + + When the caller gives a `len` of 0, it represents a request to read 0 + bytes. This read should always succeed and return an empty list and + the current `stream-status`. + + 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. + """ + result = componentize_py_runtime.call_import(7, [self, len], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + 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, identical to `read`. + """ + result = componentize_py_runtime.call_import(8, [self, len], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def skip(self, len: int) -> int: + """ + Skip bytes from a stream. + + This is similar to the `read` function, but avoids copying the + bytes into the instance. + + Once a stream has reached the end, subsequent calls to read or + `skip` will always report end-of-stream rather than producing more + data. + + This function returns the number of bytes skipped, along with a + `stream-status` indicating whether the end of the stream was + reached. The returned value will be at most `len`; it may be less. + """ + result = componentize_py_runtime.call_import(9, [self, len], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + 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`. + """ + result = componentize_py_runtime.call_import(10, [self, len], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + 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. + """ + result = componentize_py_runtime.call_import(11, [self], 1) + return result[0] + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +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`. + """ + + 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. + """ + result = componentize_py_runtime.call_import(12, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def write(self, contents: bytes) -> None: + """ + Perform a write. This function never blocks. + + 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. + """ + result = componentize_py_runtime.call_import(13, [self, contents], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + 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 + poll-one(pollable); + 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` + poll-one(pollable); + // Check for any errors that arose during `flush` + let _ = this.check-write(); // eliding error handling + ``` + """ + result = componentize_py_runtime.call_import(14, [self, contents], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + 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. + """ + result = componentize_py_runtime.call_import(15, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def blocking_flush(self) -> None: + """ + Request to flush buffered output, and block until flush completes + and stream is ready for writing again. + """ + result = componentize_py_runtime.call_import(16, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def subscribe(self) -> poll.Pollable: + """ + Create a `pollable` which will resolve once the output-stream + is ready for more writing, or an error has occured. 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. + """ + result = componentize_py_runtime.call_import(17, [self], 1) + return result[0] + + 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. + """ + result = componentize_py_runtime.call_import(18, [self, len], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + 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 + poll-one(pollable); + 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` + poll-one(pollable); + // Check for any errors that arose during `flush` + let _ = this.check-write(); // eliding error handling + ``` + """ + result = componentize_py_runtime.call_import(19, [self, len], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def splice(self, src: InputStream, len: int) -> int: + """ + Read from one stream and write to another. + + This function returns the number of bytes transferred; it may be less + than `len`. + + Unlike other I/O functions, this function blocks until all the data + read from the input stream has been written to the output stream. + """ + result = componentize_py_runtime.call_import(20, [self, src, len], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + 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 at least + one byte can be read. + """ + result = componentize_py_runtime.call_import(21, [self, src, len], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def forward(self, src: InputStream) -> int: + """ + Forward the entire contents of an input stream to an output stream. + + This function repeatedly reads from the input stream and writes + the data to the output stream, until the end of the input stream + is reached, or an error is encountered. + + Unlike other I/O functions, this function blocks until the end + of the input stream is seen and all the data has been written to + the output stream. + + This function returns the number of bytes transferred, and the status of + the output stream. + """ + result = componentize_py_runtime.call_import(22, [self, src], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + + diff --git a/python-example/http_app/imports/types.py b/python-example/http_app/imports/types.py new file mode 100644 index 0000000..3cbddd0 --- /dev/null +++ b/python-example/http_app/imports/types.py @@ -0,0 +1,373 @@ +from typing import TypeVar, Generic, Union, Optional, Union, Protocol, Tuple, List, Any +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + +from ..types import Result, Ok, Err, Some +import componentize_py_runtime +from ..imports import poll +from ..imports import streams + + +@dataclass +class MethodGet: + pass + + +@dataclass +class MethodHead: + pass + + +@dataclass +class MethodPost: + pass + + +@dataclass +class MethodPut: + pass + + +@dataclass +class MethodDelete: + pass + + +@dataclass +class MethodConnect: + pass + + +@dataclass +class MethodOptions: + pass + + +@dataclass +class MethodTrace: + pass + + +@dataclass +class MethodPatch: + pass + + +@dataclass +class MethodOther: + value: str + + +Method = Union[MethodGet, MethodHead, MethodPost, MethodPut, MethodDelete, MethodConnect, MethodOptions, MethodTrace, MethodPatch, MethodOther] + + +@dataclass +class SchemeHttp: + pass + + +@dataclass +class SchemeHttps: + pass + + +@dataclass +class SchemeOther: + value: str + + +Scheme = Union[SchemeHttp, SchemeHttps, SchemeOther] + + +@dataclass +class ErrorInvalidUrl: + value: str + + +@dataclass +class ErrorTimeoutError: + value: str + + +@dataclass +class ErrorProtocolError: + value: str + + +@dataclass +class ErrorUnexpectedError: + value: str + + +Error = Union[ErrorInvalidUrl, ErrorTimeoutError, ErrorProtocolError, ErrorUnexpectedError] + +class Fields: + + def __init__(self, entries: List[Tuple[str, bytes]]): + tmp = componentize_py_runtime.call_import(33, [entries], 1)[0] + (_, func, args, _) = tmp.finalizer.detach() + self.handle = tmp.handle + self.finalizer = weakref.finalize(self, func, args[0], args[1]) + + def get(self, name: str) -> List[bytes]: + result = componentize_py_runtime.call_import(34, [self, name], 1) + return result[0] + + def set(self, name: str, value: List[bytes]) -> None: + result = componentize_py_runtime.call_import(35, [self, name, value], 0) + return + + def delete(self, name: str) -> None: + result = componentize_py_runtime.call_import(36, [self, name], 0) + return + + def append(self, name: str, value: bytes) -> None: + result = componentize_py_runtime.call_import(37, [self, name, value], 0) + return + + def entries(self) -> List[Tuple[str, bytes]]: + result = componentize_py_runtime.call_import(38, [self], 1) + return result[0] + + def clone(self) -> Fields: + result = componentize_py_runtime.call_import(39, [self], 1) + return result[0] + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +class IncomingRequest: + + def method(self) -> Method: + result = componentize_py_runtime.call_import(40, [self], 1) + return result[0] + + def path_with_query(self) -> Optional[str]: + result = componentize_py_runtime.call_import(41, [self], 1) + return result[0] + + def scheme(self) -> Optional[Scheme]: + result = componentize_py_runtime.call_import(42, [self], 1) + return result[0] + + def authority(self) -> Optional[str]: + result = componentize_py_runtime.call_import(43, [self], 1) + return result[0] + + def headers(self) -> Fields: + result = componentize_py_runtime.call_import(44, [self], 1) + return result[0] + + def consume(self) -> IncomingBody: + result = componentize_py_runtime.call_import(45, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +class OutgoingRequest: + + def __init__(self, method: Method, path_with_query: Optional[str], scheme: Optional[Scheme], authority: Optional[str], headers: Fields): + tmp = componentize_py_runtime.call_import(46, [method, path_with_query, scheme, authority, headers], 1)[0] + (_, func, args, _) = tmp.finalizer.detach() + self.handle = tmp.handle + self.finalizer = weakref.finalize(self, func, args[0], args[1]) + + def write(self) -> OutgoingBody: + result = componentize_py_runtime.call_import(47, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +@dataclass +class RequestOptions: + connect_timeout_ms: Optional[int] + first_byte_timeout_ms: Optional[int] + between_bytes_timeout_ms: Optional[int] + +class ResponseOutparam: + + @staticmethod + def set(param: ResponseOutparam, response: Result[OutgoingResponse, Error]) -> None: + result = componentize_py_runtime.call_import(48, [param, response], 0) + return + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +class IncomingResponse: + + def status(self) -> int: + result = componentize_py_runtime.call_import(49, [self], 1) + return result[0] + + def headers(self) -> Fields: + result = componentize_py_runtime.call_import(50, [self], 1) + return result[0] + + def consume(self) -> IncomingBody: + result = componentize_py_runtime.call_import(51, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +class IncomingBody: + + def stream(self) -> streams.InputStream: + result = componentize_py_runtime.call_import(52, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + @staticmethod + def finish(this: IncomingBody) -> FutureTrailers: + result = componentize_py_runtime.call_import(53, [this], 1) + return result[0] + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +class FutureTrailers: + + def subscribe(self) -> poll.Pollable: + """ + Pollable that resolves when the body has been fully read, and the trailers + are ready to be consumed. + """ + result = componentize_py_runtime.call_import(54, [self], 1) + return result[0] + + def get(self) -> Optional[Result[Fields, Error]]: + """ + Retrieve reference to trailers, if they are ready. + """ + result = componentize_py_runtime.call_import(55, [self], 1) + return result[0] + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +class OutgoingResponse: + + def __init__(self, status_code: int, headers: Fields): + tmp = componentize_py_runtime.call_import(56, [status_code, headers], 1)[0] + (_, func, args, _) = tmp.finalizer.detach() + self.handle = tmp.handle + self.finalizer = weakref.finalize(self, func, args[0], args[1]) + + def write(self) -> OutgoingBody: + """ + Will give the child outgoing-response at most once. subsequent calls will + return an error. + """ + result = componentize_py_runtime.call_import(57, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +class OutgoingBody: + + def write(self) -> streams.OutputStream: + """ + Will give the child output-stream at most once. subsequent calls will + return an error. + """ + result = componentize_py_runtime.call_import(58, [self], 1) + if isinstance(result[0], Err): + raise result[0] + else: + return result[0].value + + @staticmethod + def finish(this: OutgoingBody, trailers: Optional[Fields]) -> None: + """ + Finalize an outgoing body, optionally providing trailers. This must be + called to signal that the response is complete. If the `outgoing-body` is + dropped without calling `outgoing-body-finalize`, the implementation + should treat the body as corrupted. + """ + result = componentize_py_runtime.call_import(59, [this, trailers], 0) + return + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + +class FutureIncomingResponse: + """ + The following block defines a special resource type used by the + `wasi:http/outgoing-handler` interface to emulate + `future>` in advance of Preview3. Given a + `future-incoming-response`, the client can call the non-blocking `get` + method to get the result if it is available. If the result is not available, + the client can call `listen` to get a `pollable` that can be passed to + `wasi:io/poll.poll-list`. + """ + + def get(self) -> Optional[Result[Result[IncomingResponse, Error], None]]: + """ + option indicates readiness. + outer result indicates you are allowed to get the + incoming-response-or-error at most once. subsequent calls after ready + will return an error here. + inner result indicates whether the incoming-response was available, or an + error occured. + """ + result = componentize_py_runtime.call_import(60, [self], 1) + return result[0] + + def subscribe(self) -> poll.Pollable: + result = componentize_py_runtime.call_import(61, [self], 1) + return result[0] + + def drop(self): + (_, func, args, _) = self.finalizer.detach() + self.handle = None + func(args[0], args[1]) + + + diff --git a/python-example/http_app/types.py b/python-example/http_app/types.py new file mode 100644 index 0000000..44f5aa3 --- /dev/null +++ b/python-example/http_app/types.py @@ -0,0 +1,24 @@ +from typing import TypeVar, Generic, Union, Optional, Union, Protocol, Tuple, List, Any +from enum import Flag, Enum, auto +from dataclasses import dataclass +from abc import abstractmethod +import weakref + + +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]] + \ No newline at end of file diff --git a/python-example/spin.toml b/python-example/spin.toml new file mode 100644 index 0000000..5e8ef87 --- /dev/null +++ b/python-example/spin.toml @@ -0,0 +1,16 @@ +spin_manifest_version = "1" +authors = ["Michelle Dhanani "] +description = "" +name = "pythonexample" +trigger = { type = "http", base = "/" } +version = "0.1.0" + +[[component]] +id = "pythonexample" +source = "app.wasm" +allowed_http_hosts = ["github.com", "api.github.com"] +[component.trigger] +route = "/..." +[component.build] +command = "componentize-py -d wit -w http-app componentize app -o py.wasm && wasm-tools compose ../github-oauth/target/wasm32-wasi/release/github_oauth.wasm -d py.wasm -o app.wasm" +watch = ["app.py", "Pipfile"] diff --git a/python-example/wit/deps/http/incoming-handler.wit b/python-example/wit/deps/http/incoming-handler.wit new file mode 100644 index 0000000..70a6a04 --- /dev/null +++ b/python-example/wit/deps/http/incoming-handler.wit @@ -0,0 +1,24 @@ +// The `wasi:http/incoming-handler` interface is meant to be exported by +// components and called by the host in response to a new incoming HTTP +// response. +// +// NOTE: in Preview3, this interface will be merged with +// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface +// that takes a `request` parameter and returns a `response` result. +// +interface incoming-handler { + use types.{incoming-request, response-outparam} + + // The `handle` function takes an outparam instead of returning its response + // so that the component may stream its response while streaming any other + // request or response bodies. The callee MUST write a response to the + // `response-outparam` and then finish the response before returning. The `handle` + // function is allowed to continue execution after finishing the response's + // output stream. While this post-response execution is taken off the + // critical path, since there is no return value, there is no way to report + // its success or failure. + handle: func( + request: incoming-request, + response-out: response-outparam + ) +} diff --git a/python-example/wit/deps/http/outgoing-handler.wit b/python-example/wit/deps/http/outgoing-handler.wit new file mode 100644 index 0000000..9b6a73c --- /dev/null +++ b/python-example/wit/deps/http/outgoing-handler.wit @@ -0,0 +1,20 @@ +// The `wasi:http/outgoing-handler` interface is meant to be imported by +// components and implemented by the host. +// +// NOTE: in Preview3, this interface will be merged with +// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface +// that takes a `request` parameter and returns a `response` result. +// +interface outgoing-handler { + use types.{outgoing-request, request-options, future-incoming-response, error} + + // The parameter and result types of the `handle` function allow the caller + // to concurrently stream the bodies of the outgoing request and the incoming + // response. + // Consumes the outgoing-request. Gives an error if the outgoing-request + // is invalid or cannot be satisfied by this handler. + handle: func( + request: outgoing-request, + options: option + ) -> result +} diff --git a/python-example/wit/deps/http/types.wit b/python-example/wit/deps/http/types.wit new file mode 100644 index 0000000..9563efd --- /dev/null +++ b/python-example/wit/deps/http/types.wit @@ -0,0 +1,214 @@ +// The `wasi:http/types` interface is meant to be imported by components to +// define the HTTP resource types and operations used by the component's +// imported and exported interfaces. +interface types { + use wasi:io/streams@0.2.0-rc-2023-10-18.{input-stream, output-stream} + use wasi:io/poll@0.2.0-rc-2023-10-18.{pollable} + + // This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + // This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string) + } + + // TODO: perhaps better align with HTTP semantics? + // This type enumerates the different kinds of errors that may occur when + // initially returning a response. + variant error { + invalid-url(string), + timeout-error(string), + protocol-error(string), + unexpected-error(string) + } + + // This following block defines the `fields` resource which corresponds to + // HTTP standard Fields. Soon, when resource types are added, the `type + // fields = u32` type alias can be replaced by a proper `resource fields` + // definition containing all the functions using the method syntactic sugar. + resource fields { + // Multiple values for a header are multiple entries in the list with the + // same key. + constructor(entries: list>>) + + // Values off wire are not necessarily well formed, so they are given by + // list instead of string. + get: func(name: string) -> list> + + // Values off wire are not necessarily well formed, so they are given by + // list instead of string. + set: func(name: string, value: list>) + delete: func(name: string) + append: func(name: string, value: list) + + // Values off wire are not necessarily well formed, so they are given by + // list instead of string. + entries: func() -> list>> + + // Deep copy of all contents in a fields. + clone: func() -> fields + } + + type headers = fields + type trailers = fields + + // The following block defines the `incoming-request` and `outgoing-request` + // resource types that correspond to HTTP standard Requests. Soon, when + // resource types are added, the `u32` type aliases can be replaced by + // proper `resource` type definitions containing all the functions as + // methods. Later, Preview2 will allow both types to be merged together into + // a single `request` type (that uses the single `stream` type mentioned + // above). The `consume` and `write` methods may only be called once (and + // return failure thereafter). + resource incoming-request { + method: func() -> method + + path-with-query: func() -> option + + scheme: func() -> option + + authority: func() -> option + + headers: func() -> /* child */ headers + // Will return the input-stream child at most once. If called more than + // once, subsequent calls will return error. + + consume: func() -> result + } + + resource outgoing-request { + constructor( + method: method, + path-with-query: option, + scheme: option, + authority: option, + headers: borrow + ) + + // Will return the outgoing-body child at most once. If called more than + // once, subsequent calls will return error. + write: func() -> result< /* child */ outgoing-body> + } + + // Additional optional parameters that can be set when making a request. + record request-options { + // The following timeouts are specific to the HTTP protocol and work + // independently of the overall timeouts passed to `io.poll.poll-list`. + + // The timeout for the initial connect. + connect-timeout-ms: option, + + // The timeout for receiving the first byte of the response body. + first-byte-timeout-ms: option, + + // The timeout for receiving the next chunk of bytes in the response body + // stream. + between-bytes-timeout-ms: option + } + + // The following block defines a special resource type used by the + // `wasi:http/incoming-handler` interface. When resource types are added, this + // block can be replaced by a proper `resource response-outparam { ... }` + // definition. Later, with Preview3, the need for an outparam goes away entirely + // (the `wasi:http/handler` interface used for both incoming and outgoing can + // simply return a `stream`). + resource response-outparam { + set: static func(param: response-outparam, response: result) + } + + // This type corresponds to the HTTP standard Status Code. + type status-code = u16 + + // The following block defines the `incoming-response` and `outgoing-response` + // resource types that correspond to HTTP standard Responses. Soon, when + // resource types are added, the `u32` type aliases can be replaced by proper + // `resource` type definitions containing all the functions as methods. Later, + // Preview2 will allow both types to be merged together into a single `response` + // type (that uses the single `stream` type mentioned above). The `consume` and + // `write` methods may only be called once (and return failure thereafter). + resource incoming-response { + status: func() -> status-code + + headers: func() -> /* child */ headers + + // May be called at most once. returns error if called additional times. + // TODO: make incoming-request-consume work the same way, giving a child + // incoming-body. + consume: func() -> result + } + + resource incoming-body { + // returned input-stream is a child - the implementation may trap if + // incoming-body is dropped (or consumed by call to + // incoming-body-finish) before the input-stream is dropped. + // May be called at most once. returns error if called additional times. + %stream: func() -> result + + // takes ownership of incoming-body. this will trap if the + // incoming-body-stream child is still alive! + finish: static func(this: incoming-body) -> + /* transitive child of the incoming-response of incoming-body */ future-trailers + } + + resource future-trailers { + /// Pollable that resolves when the body has been fully read, and the trailers + /// are ready to be consumed. + subscribe: func() -> /* child */ pollable + + /// Retrieve reference to trailers, if they are ready. + get: func() -> option> + } + + resource outgoing-response { + constructor(status-code: status-code, headers: borrow) + + /// Will give the child outgoing-response at most once. subsequent calls will + /// return an error. + write: func() -> result + } + + resource outgoing-body { + /// Will give the child output-stream at most once. subsequent calls will + /// return an error. + write: func() -> result + + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` is + /// dropped without calling `outgoing-body-finalize`, the implementation + /// should treat the body as corrupted. + finish: static func(this: outgoing-body, trailers: option) + } + + /// The following block defines a special resource type used by the + /// `wasi:http/outgoing-handler` interface to emulate + /// `future>` in advance of Preview3. Given a + /// `future-incoming-response`, the client can call the non-blocking `get` + /// method to get the result if it is available. If the result is not available, + /// the client can call `listen` to get a `pollable` that can be passed to + /// `wasi:io/poll.poll-list`. + resource future-incoming-response { + /// option indicates readiness. + /// outer result indicates you are allowed to get the + /// incoming-response-or-error at most once. subsequent calls after ready + /// will return an error here. + /// inner result indicates whether the incoming-response was available, or an + /// error occured. + get: func() -> option>> + + subscribe: func() -> /* child */ pollable + } +} diff --git a/python-example/wit/deps/http/world.wit b/python-example/wit/deps/http/world.wit new file mode 100644 index 0000000..0798c84 --- /dev/null +++ b/python-example/wit/deps/http/world.wit @@ -0,0 +1 @@ +package wasi:http@0.2.0-rc-2023-10-18 diff --git a/python-example/wit/deps/io/poll.wit b/python-example/wit/deps/io/poll.wit new file mode 100644 index 0000000..4ff4765 --- /dev/null +++ b/python-example/wit/deps/io/poll.wit @@ -0,0 +1,32 @@ +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +interface poll { + /// A "pollable" handle. + resource pollable + + /// 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. + /// + /// If the list contains more elements than can be indexed with a `u32` + /// value, this function traps. + /// + /// 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 reaedy for I/O. + poll-list: func(in: list>) -> list + + /// Poll for completion on a single pollable. + /// + /// This function is similar to `poll-list`, but operates on only a single + /// pollable. When it returns, the handle is ready for I/O. + poll-one: func(in: borrow) +} diff --git a/python-example/wit/deps/io/streams.wit b/python-example/wit/deps/io/streams.wit new file mode 100644 index 0000000..d5c7835 --- /dev/null +++ b/python-example/wit/deps/io/streams.wit @@ -0,0 +1,287 @@ +/// 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. +interface streams { + use poll.{pollable} + + /// An error for input-stream and output-stream operations. + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// Contextual error information about the last failure that happened on + /// a read, write, or flush from an `input-stream` or `output-stream`. + /// + /// This type is returned through the `stream-error` type whenever an + /// operation on a stream directly fails or an error is discovered + /// after-the-fact, for example when a write's failure shows up through a + /// later `flush` or `check-write`. + /// + /// Interfaces such as `wasi:filesystem/types` provide functionality to + /// further "downcast" this error into interface-specific error information. + resource error { + /// Returns a string that's suitable to assist humans in debugging this + /// error. + /// + /// The returned string will change across platforms and hosts which + /// means that parsing it, for example, would be a + /// platform-compatibility hazard. + to-debug-string: func() -> string + } + + /// 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`. + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a `stream-status` which, indicates whether further + /// reads are expected to produce data. The returned list will contain up to + /// `len` bytes; it may return fewer than requested, but not more. An + /// empty list and `stream-status:open` indicates no more data is + /// available at this time, and that the pollable given by `subscribe` + /// will be ready when more data is available. + /// + /// Once a stream has reached the end, subsequent calls to `read` or + /// `skip` will always report `stream-status:ended` rather than producing more + /// data. + /// + /// When the caller gives a `len` of 0, it represents a request to read 0 + /// bytes. This read should always succeed and return an empty list and + /// the current `stream-status`. + /// + /// 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. + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error> + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, identical to `read`. + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error> + + /// Skip bytes from a stream. + /// + /// This is similar to the `read` function, but avoids copying the + /// bytes into the instance. + /// + /// Once a stream has reached the end, subsequent calls to read or + /// `skip` will always report end-of-stream rather than producing more + /// data. + /// + /// This function returns the number of bytes skipped, along with a + /// `stream-status` indicating whether the end of the stream was + /// reached. The returned value will be at most `len`; it may be less. + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result + + /// 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. + subscribe: func() -> pollable + } + + + /// 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`. + resource output-stream { + /// 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. + check-write: func() -> result + + /// Perform a write. This function never blocks. + /// + /// 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. + write: func( + contents: list + ) -> result<_, stream-error> + + /// 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 + /// poll-one(pollable); + /// 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` + /// poll-one(pollable); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error> + + /// 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. + flush: func() -> result<_, stream-error> + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + blocking-flush: func() -> result<_, stream-error> + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occured. 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. + subscribe: func() -> pollable + + /// 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. + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error> + + /// 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 + /// poll-one(pollable); + /// 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` + /// poll-one(pollable); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error> + + /// Read from one stream and write to another. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + /// + /// Unlike other I/O functions, this function blocks until all the data + /// read from the input stream has been written to the output stream. + splice: func( + /// The stream to read from + src: input-stream, + /// The number of bytes to splice + len: u64, + ) -> result + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until at least + /// one byte can be read. + blocking-splice: func( + /// The stream to read from + src: input-stream, + /// The number of bytes to splice + len: u64, + ) -> result + + /// Forward the entire contents of an input stream to an output stream. + /// + /// This function repeatedly reads from the input stream and writes + /// the data to the output stream, until the end of the input stream + /// is reached, or an error is encountered. + /// + /// Unlike other I/O functions, this function blocks until the end + /// of the input stream is seen and all the data has been written to + /// the output stream. + /// + /// This function returns the number of bytes transferred, and the status of + /// the output stream. + forward: func( + /// The stream to read from + src: input-stream + ) -> result + } +} diff --git a/python-example/wit/deps/io/world.wit b/python-example/wit/deps/io/world.wit new file mode 100644 index 0000000..0fdcfcd --- /dev/null +++ b/python-example/wit/deps/io/world.wit @@ -0,0 +1 @@ +package wasi:io@0.2.0-rc-2023-10-18 diff --git a/python-example/wit/world.wit b/python-example/wit/world.wit new file mode 100644 index 0000000..d4031ad --- /dev/null +++ b/python-example/wit/world.wit @@ -0,0 +1,5 @@ +package example:app; + +world http-app { + export wasi:http/incoming-handler@0.2.0-rc-2023-10-18; +} \ No newline at end of file