From a68f4ebace26e1745e53bab90b03eb5074494a40 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Mon, 2 Feb 2026 10:28:47 -0500 Subject: [PATCH 1/2] Allow hand-polishing individual exceptions by overriding templates. Fix #37. Drop a file into `generate_patches/templates` parallel to an exception module from `fastly_compute/exceptions`, and you can add improved exception implementations there. Disable the generation of stock ones by passing their names in the `generated_exception()` call, e.g. `generated_exceptions(omit=["SomeException", "SomeOtherException"])`. * Move almost all templating into Jinja templates. * Break generated exception code into named chunks that can be omitted individually by a customizing template. * Slightly improve `BufferLen` exception as an example. --- Makefile | 2 +- examples/backend-requests/uv.lock | 5 +- examples/bottle-app/uv.lock | 5 +- examples/flask-app/uv.lock | 5 +- examples/game-of-life/uv.lock | 5 +- pyproject.toml | 1 + scripts/generate_patches/generation.py | 165 +++++++++--------- .../templates/default_exception.py.jinja | 10 ++ .../templates/empty_init.py.jinja | 1 + .../templates/exceptions/types/error.py.jinja | 12 ++ .../templates/patches.py.jinja | 38 ++++ uv.lock | 6 +- 12 files changed, 164 insertions(+), 91 deletions(-) create mode 100644 scripts/generate_patches/templates/default_exception.py.jinja create mode 100644 scripts/generate_patches/templates/empty_init.py.jinja create mode 100644 scripts/generate_patches/templates/exceptions/types/error.py.jinja create mode 100644 scripts/generate_patches/templates/patches.py.jinja diff --git a/Makefile b/Makefile index 4815115..7a5c170 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ $(BUILD_DIR)/%.composed.wasm: wit/viceroy.wit wit/deps/fastly/compute.wit fastly # The script that writes the exceptions and the patches always rewrites # everything, so we can depend on the mod date of only 1 file. We choose # patches.py, because its name doesn't depend on the WIT contents. -fastly_compute/runtime_patching/patches.py: scripts/generate_patches/*.py $(COMPUTE_WIT) +fastly_compute/runtime_patching/patches.py: scripts/generate_patches/*.py $(shell find scripts/generate_patches/templates -name "*.jinja") $(COMPUTE_WIT) uv run python -m scripts.generate_patches # Create build directory diff --git a/examples/backend-requests/uv.lock b/examples/backend-requests/uv.lock index b9290fb..88d12b0 100644 --- a/examples/backend-requests/uv.lock +++ b/examples/backend-requests/uv.lock @@ -46,4 +46,7 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] diff --git a/examples/bottle-app/uv.lock b/examples/bottle-app/uv.lock index 9d45ce9..05dbfde 100644 --- a/examples/bottle-app/uv.lock +++ b/examples/bottle-app/uv.lock @@ -46,4 +46,7 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] diff --git a/examples/flask-app/uv.lock b/examples/flask-app/uv.lock index 9f60925..1358147 100644 --- a/examples/flask-app/uv.lock +++ b/examples/flask-app/uv.lock @@ -52,7 +52,10 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] [[package]] name = "flask" diff --git a/examples/game-of-life/uv.lock b/examples/game-of-life/uv.lock index 00cb4e1..d401e93 100644 --- a/examples/game-of-life/uv.lock +++ b/examples/game-of-life/uv.lock @@ -52,7 +52,10 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] [[package]] name = "flask" diff --git a/pyproject.toml b/pyproject.toml index 62517d8..706f3d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ build-backend = "maturin" [dependency-groups] dev = [ + "jinja2>=3.1.6", "maturin>=1.11.5", ] diff --git a/scripts/generate_patches/generation.py b/scripts/generate_patches/generation.py index 4d8f742..32abdc0 100755 --- a/scripts/generate_patches/generation.py +++ b/scripts/generate_patches/generation.py @@ -7,15 +7,25 @@ import json from collections import defaultdict from collections.abc import Iterable, Mapping +from functools import partial from pathlib import Path from subprocess import check_output +from typing import Any + +from jinja2 import Environment, PackageLoader, Template, TemplateNotFound from .wit import Function, NullType, Type, Wit WIT_DIR = "wit" +FASTLY_COMPUTE = Path(__file__).parent.parent.parent / "fastly_compute" + + +jinja_env = Environment( + loader=PackageLoader("scripts.generate_patches"), autoescape=False +) -def exception_code_tree(error_types: Iterable[Type]) -> Mapping[str, Mapping[str, str]]: +def generate_exceptions(error_types: Iterable[Type]): """Generate Python exception classes we can map error types to. Inherit names and docstrings from the WIT. Create a common superclass for @@ -28,39 +38,50 @@ def exception_code_tree(error_types: Iterable[Type]) -> Mapping[str, Mapping[str contained code. For example, acl (from the interface name) -> acl_error.py (from the enum name) -> class AclError(FastlyError)... """ - # interface name -> module name -> code chunks: - code = defaultdict(lambda: defaultdict(str)) + # package name -> module name -> code chunks: + code = defaultdict(lambda: defaultdict(dict)) + packages_to_init = set() for error_type in error_types: package = error_type.py_package() module = error_type.py_module() + ".py" # Create package's empty __init__.py if not already there: - code[package]["__init__.py"] - - if not code[package][module]: - code[package][module] += ( - "from fastly_compute.exceptions import FastlyError\n\n\n" - ) + packages_to_init.add(package) # Common superclass for exceptions based on the enum or variant's # members. Or the raised exception itself for records. - code[package][module] += ( - f"""class {error_type.py_exception_name()}(FastlyError):\n""" + top_level_exception_name = error_type.py_exception_name() + code[package][module][top_level_exception_name] = ( + f"""class {top_level_exception_name}(FastlyError):\n""" f''' """{error_type.docstring_or_pass()}"""\n\n\n''' ) # Insert enum or variant cases. for case in error_type.cases(): - code[package][module] += ( - f"""class {case.py_exception_name()}({error_type.py_exception_name()}):\n""" + case_exception_name = case.py_exception_name() + code[package][module][case_exception_name] = ( + f"""class {case_exception_name}({top_level_exception_name}):\n""" f''' """{case.docstring_or_pass()}"""\n\n\n''' ) - return code + + for package in packages_to_init: + write_templated_file( + FASTLY_COMPUTE / "exceptions" / package / "__init__.py", + {}, + jinja_env.get_template("empty_init.py.jinja"), + ) + for package, modules in code.items(): + for module, exceptions in modules.items(): + write_templated_file( + FASTLY_COMPUTE / "exceptions" / package / module, + {"generated_exceptions": partial(join_named_chunks, exceptions)}, + jinja_env.get_template("default_exception.py.jinja"), + ) -def mappings_code_tree( +def generate_patches( error_types: Iterable[Type], functions_to_patch: Iterable[Function] -) -> dict[str, dict[str, str]]: +): """Generate code which makes componentize-py-generated routines raise more specific, idiomatically shaped exceptions. @@ -68,13 +89,6 @@ def mappings_code_tree( monkeypatches that wrap componentize-py's generated Python routines to raise them. """ - code = ( - """# This file is automatically generated by generate_patches.py.\n""" - """# It is not intended for manual editing.\n""" - '''"""Monkeypatches which wrap the routines generated by componentize-py to make\n''' - '''them raise more specific exceptions, not just Err."""\n\n''' - ) - # Collect info: mappings = set() imports = set() @@ -106,68 +120,56 @@ def mappings_code_tree( for func in functions_to_patch: imports.add(func.wit_module_path()) - # Do templating: - code += "try:\n" - code += ( - " from .decorators import remap_wit_errors\n" - " import fastly_compute.exceptions\n" - ) - for import_ in sorted(imports): - code += f" import {import_}\n" - code += ( - "except ImportError:\n" - " # Tolerate that momentary import for the testrunner before Viceroy, and thus\n" - " # the wit_world, is around.\n" - " def patch():\n" - ' print("Faking the run of exception-mapping monkeypatches for test runner.")\n' - "else:\n" - " MAPPINGS = {\n" - ) - for wit_path, py_module_path, py_exception_name in sorted(mappings): - code += f" {wit_path}: {py_module_path}.{py_exception_name},\n" - code += ( - " type(None): fastly_compute.exceptions.FastlyError,\n" # Linter: don't wrap. - " }\n" - ) - - code += ''' - did_patch = False - - def patch(): - """Apply patches if they haven't already been applied.""" + # TODO: Maybe automatically improve the docstring of each method to list the + # exceptions it raises. - global did_patch - if did_patch: - # This test shouldn't be needed, but it avoids double-wrapping the - # routines if somehow patch() did get called twice. - return - did_patch = True\n\n''' + write_templated_file( + FASTLY_COMPUTE / "runtime_patching" / "patches.py", + { + "imports": imports, + "mappings": sorted(mappings), + "functions_to_patch": functions_to_patch, + }, + jinja_env.get_template("patches.py.jinja"), + ) - for func in functions_to_patch: - func_path = func.wit_path() - code += f" {func_path} = remap_wit_errors(MAPPINGS)({func_path})\n" - # TODO: Make affordance for manually adding ergonomic getter properties, - # __str__s, etc. to exception classes. +def join_named_chunks(chunks: dict[str, str], omit: list[str] | None = None) -> str: + """Return an ordered concatenation of all items in a dict except those of + the given keys. + """ + if omit is None: + omit = [] + return "".join( + chunk for name, chunk in chunks.items() if name not in omit + ) # O(n^2) but small - # TODO: Maybe automatically improve the docstring of each method to list the - # exceptions it raises. - return {"runtime_patching": {"patches.py": code}} +def write_templated_file( + dest_file: Path, template_vars: Mapping[str, Any], default_template: Template +): + """Render templates to generate code on disk, providing hook points for + replacing generated pieces with manual improvements. + We examine the ``templates`` folder for a file at the same relative path as + ``dest_file`` is from ``fastly_compute``. We use it if found. Otherwise, we + call back to ``default_template``. -def write_files(tree: Mapping[str, Mapping[str, str]], base_folder: Path): - """Create filesystem artifacts mirroring a nested dict representing folders, - then files, then file contents. + :arg dest_file: Path to the file to write, relative to ``fastly_compute`` + :arg template_vars: Data to populate the template + :arg default_template: Template to fall back to if a parallel one to does + not exist in the templates folder - Overwrite files that are mentioned in ``tree``, but don't delete anything else. """ - for folder, files in tree.items(): - folder_path = base_folder / folder - folder_path.mkdir(parents=True, exist_ok=True) - for file, contents in files.items(): - file_path = folder_path / file - file_path.write_text(contents) + subpath = dest_file.relative_to(FASTLY_COMPUTE) + try: + # Render a parallel template if it's there: + template = jinja_env.get_template(str(subpath) + ".jinja") + except TemplateNotFound: + template = default_template + rendered = template.render(template_vars) + dest_file.parent.mkdir(parents=True, exist_ok=True) + dest_file.write_text(rendered) def generate(): @@ -210,12 +212,5 @@ def generate(): # identifiable: functions_to_patch.append(function) - fastly_compute = Path(__file__).parent.parent.parent / "fastly_compute" - write_files( - exception_code_tree(exceptions_to_generate.keys()), - fastly_compute / "exceptions", - ) - write_files( - mappings_code_tree(exceptions_to_generate.keys(), functions_to_patch), - fastly_compute, - ) + generate_exceptions(exceptions_to_generate.keys()) + generate_patches(exceptions_to_generate.keys(), functions_to_patch) diff --git a/scripts/generate_patches/templates/default_exception.py.jinja b/scripts/generate_patches/templates/default_exception.py.jinja new file mode 100644 index 0000000..738a12d --- /dev/null +++ b/scripts/generate_patches/templates/default_exception.py.jinja @@ -0,0 +1,10 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +{% block imports -%} +from fastly_compute.exceptions import FastlyError +{%- endblock %} + + +{% block exceptions -%} +{{ generated_exceptions() }} +{% endblock -%} diff --git a/scripts/generate_patches/templates/empty_init.py.jinja b/scripts/generate_patches/templates/empty_init.py.jinja new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/scripts/generate_patches/templates/empty_init.py.jinja @@ -0,0 +1 @@ + diff --git a/scripts/generate_patches/templates/exceptions/types/error.py.jinja b/scripts/generate_patches/templates/exceptions/types/error.py.jinja new file mode 100644 index 0000000..aac2a1c --- /dev/null +++ b/scripts/generate_patches/templates/exceptions/types/error.py.jinja @@ -0,0 +1,12 @@ +{% extends "default_exception.py.jinja" -%} + +{% block exceptions -%} +{{ generated_exceptions(omit=["BufferLen"]) -}} + +class BufferLen(Error): + def __init__(self, wit_error): + self.length = wit_error.value + + def __str__(self): + return f"Buffer was too short to hold the result. At least {self.length} bytes are needed." +{% endblock %} diff --git a/scripts/generate_patches/templates/patches.py.jinja b/scripts/generate_patches/templates/patches.py.jinja new file mode 100644 index 0000000..af0d008 --- /dev/null +++ b/scripts/generate_patches/templates/patches.py.jinja @@ -0,0 +1,38 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""Monkeypatches which wrap the routines generated by componentize-py to make +them raise more specific exceptions, not just Err.""" + +try: + from .decorators import remap_wit_errors + {% for import_ in imports -%} + import {{ import_ }} + {% endfor %} +except ImportError: + # Tolerate that momentary import for the testrunner before Viceroy, and thus + # the wit_world, is around. + def patch(): + print("Faking the run of exception-mapping monkeypatches for test runner.") +else: + MAPPINGS = { + {% for wit_path, py_module_path, py_exception_name in mappings -%} + {{wit_path}}: {{py_module_path}}.{{py_exception_name}}, + {% endfor -%} + type(None): fastly_compute.exceptions.FastlyError, + } + + did_patch = False + + def patch(): + """Apply patches if they haven't already been applied.""" + + global did_patch + if did_patch: + # This test shouldn't be needed, but it avoids double-wrapping the + # routines if somehow patch() did get called twice. + return + did_patch = True + + {% for func in functions_to_patch -%} + {{ func.wit_path() }} = remap_wit_errors(MAPPINGS)({{ func.wit_path() }}) + {% endfor %} diff --git a/uv.lock b/uv.lock index c73e5b9..75a9734 100644 --- a/uv.lock +++ b/uv.lock @@ -144,6 +144,7 @@ test = [ [package.dev-dependencies] dev = [ + { name = "jinja2" }, { name = "maturin" }, ] @@ -162,7 +163,10 @@ requires-dist = [ provides-extras = ["test", "dev", "examples"] [package.metadata.requires-dev] -dev = [{ name = "maturin", specifier = ">=1.11.5" }] +dev = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "maturin", specifier = ">=1.11.5" }, +] [[package]] name = "flask" From 9daab774b5aaab5127acdab562ed5fc4157b5f70 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 3 Feb 2026 12:26:35 -0500 Subject: [PATCH 2/2] Commit modules emitted by `generate_patches`. This innately gives us diffs to look at, filling the role of tests. Also... * Make all but `patches.py` pass `ruff lint` and `ruff format`. (`ruff format` does complex line wrapping to `patches.py`.) * Add interface-level docstrings to packages' `__init__.py`s. `Interface` becomes a `DocsHaver` to support this. --- .gitignore | 3 - fastly_compute/exceptions/acl/__init__.py | 4 + fastly_compute/exceptions/acl/acl_error.py | 23 ++ .../exceptions/http_body/__init__.py | 1 + .../exceptions/http_body/trailer_error.py | 21 ++ .../exceptions/http_req/__init__.py | 9 + .../exceptions/kv_store/__init__.py | 7 + .../exceptions/kv_store/kv_error.py | 57 ++++ fastly_compute/exceptions/types/__init__.py | 1 + fastly_compute/exceptions/types/error.py | 116 ++++++++ fastly_compute/exceptions/types/open_error.py | 47 +++ fastly_compute/runtime_patching/patches.py | 276 ++++++++++++++++++ pyproject.toml | 2 + scripts/generate_patches/generation.py | 62 ++-- .../templates/default_exception.py.jinja | 2 + .../templates/empty_init.py.jinja | 1 - .../templates/exception_init_module.py.jinja | 2 + .../templates/exceptions/types/error.py.jinja | 9 +- .../templates/patches.py.jinja | 21 +- scripts/generate_patches/utils.py | 12 - scripts/generate_patches/wit.py | 27 +- 21 files changed, 648 insertions(+), 55 deletions(-) create mode 100644 fastly_compute/exceptions/acl/__init__.py create mode 100644 fastly_compute/exceptions/acl/acl_error.py create mode 100644 fastly_compute/exceptions/http_body/__init__.py create mode 100644 fastly_compute/exceptions/http_body/trailer_error.py create mode 100644 fastly_compute/exceptions/http_req/__init__.py create mode 100644 fastly_compute/exceptions/kv_store/__init__.py create mode 100644 fastly_compute/exceptions/kv_store/kv_error.py create mode 100644 fastly_compute/exceptions/types/__init__.py create mode 100644 fastly_compute/exceptions/types/error.py create mode 100644 fastly_compute/exceptions/types/open_error.py create mode 100644 fastly_compute/runtime_patching/patches.py delete mode 100644 scripts/generate_patches/templates/empty_init.py.jinja create mode 100644 scripts/generate_patches/templates/exception_init_module.py.jinja diff --git a/.gitignore b/.gitignore index f299143..8543346 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ __pycache__ # Generated code /stubs/ -/fastly_compute/exceptions/* -!/fastly_compute/exceptions/__init__.py -/fastly_compute/runtime_patching/patches.py # Build artifacts /build/ diff --git a/fastly_compute/exceptions/acl/__init__.py b/fastly_compute/exceptions/acl/__init__.py new file mode 100644 index 0000000..7931326 --- /dev/null +++ b/fastly_compute/exceptions/acl/__init__.py @@ -0,0 +1,4 @@ +"""Blocklists using [Access Control Lists] (ACLs) + +[Access Control Lists]: https://www.fastly.com/documentation/reference/api/acls/ +""" diff --git a/fastly_compute/exceptions/acl/acl_error.py b/fastly_compute/exceptions/acl/acl_error.py new file mode 100644 index 0000000..1d6a73b --- /dev/null +++ b/fastly_compute/exceptions/acl/acl_error.py @@ -0,0 +1,23 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""Errors returned on ACL lookup failure.""" + +from fastly_compute.exceptions import FastlyError + + +class AclError(FastlyError): + """Errors returned on ACL lookup failure.""" + + +class TooManyRequests(AclError): + """Too many requests have been made. + + This corresponds to an HTTP error code of 429, “Too Many Requests”. + """ + + +class GenericError(AclError): + """Generic error value. + + This means that some unexpected error occurred. + """ diff --git a/fastly_compute/exceptions/http_body/__init__.py b/fastly_compute/exceptions/http_body/__init__.py new file mode 100644 index 0000000..96f6454 --- /dev/null +++ b/fastly_compute/exceptions/http_body/__init__.py @@ -0,0 +1 @@ +"""HTTP bodies.""" diff --git a/fastly_compute/exceptions/http_body/trailer_error.py b/fastly_compute/exceptions/http_body/trailer_error.py new file mode 100644 index 0000000..7fb4227 --- /dev/null +++ b/fastly_compute/exceptions/http_body/trailer_error.py @@ -0,0 +1,21 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""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. +""" + +from fastly_compute.exceptions import FastlyError + + +class TrailerError(FastlyError): + """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. + """ + + +class NotAvailableYet(TrailerError): + """The trailers aren't available yet.""" + + +class Error(TrailerError): + """An error occurred.""" diff --git a/fastly_compute/exceptions/http_req/__init__.py b/fastly_compute/exceptions/http_req/__init__.py new file mode 100644 index 0000000..eeab097 --- /dev/null +++ b/fastly_compute/exceptions/http_req/__init__.py @@ -0,0 +1,9 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""An `error` code, optionally with extra request error information.""" + +from fastly_compute.exceptions import FastlyError + + +class ErrorWithDetail(FastlyError): + """An `error` code, optionally with extra request error information.""" diff --git a/fastly_compute/exceptions/kv_store/__init__.py b/fastly_compute/exceptions/kv_store/__init__.py new file mode 100644 index 0000000..9f28322 --- /dev/null +++ b/fastly_compute/exceptions/kv_store/__init__.py @@ -0,0 +1,7 @@ +"""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 +""" diff --git a/fastly_compute/exceptions/kv_store/kv_error.py b/fastly_compute/exceptions/kv_store/kv_error.py new file mode 100644 index 0000000..3adf7ff --- /dev/null +++ b/fastly_compute/exceptions/kv_store/kv_error.py @@ -0,0 +1,57 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""A value indicating the status of a KV store operation.""" + +from fastly_compute.exceptions import FastlyError + + +class KvError(FastlyError): + """A value indicating the status of a KV store operation.""" + + +class BadRequest(KvError): + """KV store cannot or will not process the request due to something that is perceived to be a + client error. + + This will map to the api's 400 codes. + """ + + +class PreconditionFailed(KvError): + """KV store cannot fulfill the request, as defined by the client's prerequisites, for example + `if-generation-match`. + + This will map to the api's 412 codes. + """ + + +class PayloadTooLarge(KvError): + """The size limit for a KV store key was exceeded. + + This will map to the api's 413 codes. + """ + + +class InternalError(KvError): + """The system encountered an unexpected internal error. + + This will map to all remaining http error codes. + """ + + +class TooManyRequests(KvError): + """Too many requests have been made to the KV store. + + This will map to the api's 429 codes. + """ + + +class GenericError(KvError): + """Generic error value. + + This means that some unexpected error occurred. + """ + + +class Extra(KvError): + """Additional error information may be added in the future via this resource type.""" diff --git a/fastly_compute/exceptions/types/__init__.py b/fastly_compute/exceptions/types/__init__.py new file mode 100644 index 0000000..025dc75 --- /dev/null +++ b/fastly_compute/exceptions/types/__init__.py @@ -0,0 +1 @@ +"""Types used by many interfaces in this package.""" diff --git a/fastly_compute/exceptions/types/error.py b/fastly_compute/exceptions/types/error.py new file mode 100644 index 0000000..d31ee0b --- /dev/null +++ b/fastly_compute/exceptions/types/error.py @@ -0,0 +1,116 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""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. +""" + +from fastly_compute.exceptions import FastlyError + + +class Error(FastlyError): + """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 GenericError(Error): + """Generic error value. + + This means that some unexpected error occurred. + """ + + +class InvalidArgument(Error): + """Invalid argument.""" + + +class AuxiliaryError(Error): + """Auxiliary error value. + + For `cache.get-body` and `cache.replace-get-body`, it means the cache implementation was + busy and not ready to retrieve the body data. + + For cache APIs that attempt to write to or update the body of a cache transaction, it means + that an error occurred while attempting the write or update. + + For other cache APIs, it indicates that the underlying cache entry or cache replace entry + is no longer available. + + For writing to a streaming HTTP body, indicates that the body has already been closed. + + For a dictionary lookup, indicates that the dictionary was not found. + """ + + +class Unsupported(Error): + """Unsupported operation error. + + This error is returned when some operation cannot be performed, because it is not supported. + """ + + +class HttpInvalid(Error): + """Invalid HTTP error. + + This can be returned when a method, URI, header, or status is not valid. This can also + be returned if a message head is too large. + """ + + +class HttpUser(Error): + """HTTP user error. + + This is returned in cases where user code caused an HTTP error. For example, attempt to send + a 1xx response code, or a request with a non-absolute URI. This can also be caused by + an unexpected header: both `content-length` and `transfer-encoding`, for example. + """ + + +class HttpIncomplete(Error): + """HTTP incomplete message error. + + This can be returned when a stream ended unexpectedly. + """ + + +class CannotRead(Error): + """Cannot read. + + An error occurred while attempting to read the body of a cache transaction. + """ + + +class HttpHeadTooLarge(Error): + """Message head too large.""" + + +class HttpInvalidStatus(Error): + """Invalid HTTP status.""" + + +class LimitExceeded(Error): + """Limit exceeded + + This is returned when an attempt to allocate a resource has exceeded the maximum number of + resources permitted. For example, creating too many response handles. + """ + + +class BufferLen(Error): + """Buffer length error + + Returned when a buffer is the wrong size. + Includes the buffer length that would allow the operation to succeed. + """ + + def __init__(self, wit_error): + self.length = wit_error.value + + def __str__(self): + return f"Buffer was too short to hold the result. At least {self.length} bytes are needed." diff --git a/fastly_compute/exceptions/types/open_error.py b/fastly_compute/exceptions/types/open_error.py new file mode 100644 index 0000000..5b8acfa --- /dev/null +++ b/fastly_compute/exceptions/types/open_error.py @@ -0,0 +1,47 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""An error returned by `open`-like functions.""" + +from fastly_compute.exceptions import FastlyError + + +class OpenError(FastlyError): + """An error returned by `open`-like functions.""" + + +class InvalidSyntax(OpenError): + """The given name of the entity to open was invalid.""" + + +class NameTooLong(OpenError): + """The given name is longer the maximum permitted length.""" + + +class Reserved(OpenError): + """The given name is a reserved name that may not be opened.""" + + +class NotFound(OpenError): + """No entity by the given name was found.""" + + +class Unsupported(OpenError): + """Unsupported operation error. + + This error is returned when some operation cannot be performed, because it is not supported. + """ + + +class LimitExceeded(OpenError): + """Limit exceeded + + This is returned when an attempt to allocate a resource has exceeded the maximum number of + resources permitted. For example, creating too many response handles. + """ + + +class GenericError(OpenError): + """Generic error value. + + This means that some unexpected error occurred. + """ diff --git a/fastly_compute/runtime_patching/patches.py b/fastly_compute/runtime_patching/patches.py new file mode 100644 index 0000000..cce0296 --- /dev/null +++ b/fastly_compute/runtime_patching/patches.py @@ -0,0 +1,276 @@ +# This file is automatically generated by generate_patches. +# It is not intended for manual editing. +"""Monkeypatches which wrap the routines generated by componentize-py to make +them raise more specific exceptions, not just Err. +""" + +try: + import wit_world.imports.acl + import wit_world.imports.backend + import wit_world.imports.cache + import wit_world.imports.config_store + import wit_world.imports.device_detection + import wit_world.imports.dictionary + import wit_world.imports.erl + import wit_world.imports.geo + import wit_world.imports.http_body + import wit_world.imports.http_cache + import wit_world.imports.http_downstream + import wit_world.imports.http_req + import wit_world.imports.http_resp + import wit_world.imports.image_optimizer + import wit_world.imports.kv_store + import wit_world.imports.log + import wit_world.imports.purge + import wit_world.imports.secret_store + import wit_world.imports.security + import wit_world.imports.shielding + import wit_world.imports.types + + import fastly_compute.exceptions.acl.acl_error + import fastly_compute.exceptions.http_body.trailer_error + import fastly_compute.exceptions.http_req + import fastly_compute.exceptions.kv_store.kv_error + import fastly_compute.exceptions.types.error + import fastly_compute.exceptions.types.open_error + + from .decorators import remap_wit_errors +except ImportError: + # Tolerate that momentary import for the testrunner before Viceroy, and thus + # the wit_world, is around. + def patch(): + """Pretend to patch.""" + print("Faking the run of exception-mapping monkeypatches for test runner.") +else: + MAPPINGS = { + wit_world.imports.acl.AclError.GENERIC_ERROR: fastly_compute.exceptions.acl.acl_error.GenericError, + wit_world.imports.acl.AclError.TOO_MANY_REQUESTS: fastly_compute.exceptions.acl.acl_error.TooManyRequests, + wit_world.imports.http_body.TrailerError_Error: fastly_compute.exceptions.http_body.trailer_error.Error, + wit_world.imports.http_body.TrailerError_NotAvailableYet: fastly_compute.exceptions.http_body.trailer_error.NotAvailableYet, + wit_world.imports.http_req.ErrorWithDetail: fastly_compute.exceptions.http_req.ErrorWithDetail, + wit_world.imports.kv_store.KvError_BadRequest: fastly_compute.exceptions.kv_store.kv_error.BadRequest, + wit_world.imports.kv_store.KvError_Extra: fastly_compute.exceptions.kv_store.kv_error.Extra, + wit_world.imports.kv_store.KvError_GenericError: fastly_compute.exceptions.kv_store.kv_error.GenericError, + wit_world.imports.kv_store.KvError_InternalError: fastly_compute.exceptions.kv_store.kv_error.InternalError, + wit_world.imports.kv_store.KvError_PayloadTooLarge: fastly_compute.exceptions.kv_store.kv_error.PayloadTooLarge, + wit_world.imports.kv_store.KvError_PreconditionFailed: fastly_compute.exceptions.kv_store.kv_error.PreconditionFailed, + wit_world.imports.kv_store.KvError_TooManyRequests: fastly_compute.exceptions.kv_store.kv_error.TooManyRequests, + wit_world.imports.types.Error_AuxiliaryError: fastly_compute.exceptions.types.error.AuxiliaryError, + wit_world.imports.types.Error_BufferLen: fastly_compute.exceptions.types.error.BufferLen, + wit_world.imports.types.Error_CannotRead: fastly_compute.exceptions.types.error.CannotRead, + wit_world.imports.types.Error_GenericError: fastly_compute.exceptions.types.error.GenericError, + wit_world.imports.types.Error_HttpHeadTooLarge: fastly_compute.exceptions.types.error.HttpHeadTooLarge, + wit_world.imports.types.Error_HttpIncomplete: fastly_compute.exceptions.types.error.HttpIncomplete, + wit_world.imports.types.Error_HttpInvalid: fastly_compute.exceptions.types.error.HttpInvalid, + wit_world.imports.types.Error_HttpInvalidStatus: fastly_compute.exceptions.types.error.HttpInvalidStatus, + wit_world.imports.types.Error_HttpUser: fastly_compute.exceptions.types.error.HttpUser, + wit_world.imports.types.Error_InvalidArgument: fastly_compute.exceptions.types.error.InvalidArgument, + wit_world.imports.types.Error_LimitExceeded: fastly_compute.exceptions.types.error.LimitExceeded, + wit_world.imports.types.Error_Unsupported: fastly_compute.exceptions.types.error.Unsupported, + wit_world.imports.types.OpenError.GENERIC_ERROR: fastly_compute.exceptions.types.open_error.GenericError, + wit_world.imports.types.OpenError.INVALID_SYNTAX: fastly_compute.exceptions.types.open_error.InvalidSyntax, + wit_world.imports.types.OpenError.LIMIT_EXCEEDED: fastly_compute.exceptions.types.open_error.LimitExceeded, + wit_world.imports.types.OpenError.NAME_TOO_LONG: fastly_compute.exceptions.types.open_error.NameTooLong, + wit_world.imports.types.OpenError.NOT_FOUND: fastly_compute.exceptions.types.open_error.NotFound, + wit_world.imports.types.OpenError.RESERVED: fastly_compute.exceptions.types.open_error.Reserved, + wit_world.imports.types.OpenError.UNSUPPORTED: fastly_compute.exceptions.types.open_error.Unsupported, + type(None): fastly_compute.exceptions.FastlyError, + } + + did_patch = False + + def patch(): + """Apply patches if they haven't already been applied.""" + global did_patch + if did_patch: + # This test shouldn't be needed, but it avoids double-wrapping the + # routines if somehow patch() did get called twice. + return + did_patch = True + wit_world.imports.log.Endpoint.open = remap_wit_errors(MAPPINGS)(wit_world.imports.log.Endpoint.open) + wit_world.imports.dictionary.Dictionary.open = remap_wit_errors(MAPPINGS)(wit_world.imports.dictionary.Dictionary.open) + wit_world.imports.dictionary.Dictionary.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.dictionary.Dictionary.lookup) + wit_world.imports.geo.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.geo.lookup) + wit_world.imports.device_detection.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.device_detection.lookup) + wit_world.imports.erl.RateCounter.open = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.open) + wit_world.imports.erl.RateCounter.check_rate = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.check_rate) + wit_world.imports.erl.RateCounter.increment = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.increment) + wit_world.imports.erl.RateCounter.lookup_rate = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.lookup_rate) + wit_world.imports.erl.RateCounter.lookup_count = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.RateCounter.lookup_count) + wit_world.imports.erl.PenaltyBox.open = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.PenaltyBox.open) + wit_world.imports.erl.PenaltyBox.add = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.PenaltyBox.add) + wit_world.imports.erl.PenaltyBox.has = remap_wit_errors(MAPPINGS)(wit_world.imports.erl.PenaltyBox.has) + wit_world.imports.secret_store.Secret.from_bytes = remap_wit_errors(MAPPINGS)(wit_world.imports.secret_store.Secret.from_bytes) + wit_world.imports.secret_store.Secret.plaintext = remap_wit_errors(MAPPINGS)(wit_world.imports.secret_store.Secret.plaintext) + wit_world.imports.secret_store.Store.open = remap_wit_errors(MAPPINGS)(wit_world.imports.secret_store.Store.open) + wit_world.imports.secret_store.Store.get = remap_wit_errors(MAPPINGS)(wit_world.imports.secret_store.Store.get) + wit_world.imports.backend.register_dynamic_backend = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.register_dynamic_backend) + wit_world.imports.backend.Backend.open = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.open) + wit_world.imports.backend.Backend.is_healthy = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.is_healthy) + wit_world.imports.backend.Backend.is_dynamic = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.is_dynamic) + wit_world.imports.backend.Backend.get_host = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_host) + wit_world.imports.backend.Backend.get_override_host = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_override_host) + wit_world.imports.backend.Backend.get_port = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_port) + wit_world.imports.backend.Backend.get_connect_timeout_ms = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_connect_timeout_ms) + wit_world.imports.backend.Backend.get_first_byte_timeout_ms = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_first_byte_timeout_ms) + wit_world.imports.backend.Backend.get_between_bytes_timeout_ms = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_between_bytes_timeout_ms) + wit_world.imports.backend.Backend.is_tls = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.is_tls) + wit_world.imports.backend.Backend.get_tls_min_version = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tls_min_version) + wit_world.imports.backend.Backend.get_tls_max_version = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tls_max_version) + wit_world.imports.backend.Backend.get_http_keepalive_time = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_http_keepalive_time) + wit_world.imports.backend.Backend.get_tcp_keepalive_enable = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tcp_keepalive_enable) + wit_world.imports.backend.Backend.get_tcp_keepalive_interval = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tcp_keepalive_interval) + wit_world.imports.backend.Backend.get_tcp_keepalive_probes = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tcp_keepalive_probes) + wit_world.imports.backend.Backend.get_tcp_keepalive_time = remap_wit_errors(MAPPINGS)(wit_world.imports.backend.Backend.get_tcp_keepalive_time) + wit_world.imports.http_body.new = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.new) + wit_world.imports.http_body.append = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.append) + wit_world.imports.http_body.read = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.read) + wit_world.imports.http_body.write = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.write) + wit_world.imports.http_body.write_front = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.write_front) + wit_world.imports.http_body.close = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.close) + wit_world.imports.http_body.append_trailer = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.append_trailer) + wit_world.imports.http_body.get_trailer_names = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.get_trailer_names) + wit_world.imports.http_body.get_trailer_value = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.get_trailer_value) + wit_world.imports.http_body.get_trailer_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_body.get_trailer_values) + wit_world.imports.http_resp.Response.new = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.new) + wit_world.imports.http_resp.Response.get_header_names = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_header_names) + wit_world.imports.http_resp.Response.get_header_value = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_header_value) + wit_world.imports.http_resp.Response.get_header_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_header_values) + wit_world.imports.http_resp.Response.set_header_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_header_values) + wit_world.imports.http_resp.Response.insert_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.insert_header) + wit_world.imports.http_resp.Response.append_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.append_header) + wit_world.imports.http_resp.Response.remove_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.remove_header) + wit_world.imports.http_resp.Response.get_version = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_version) + wit_world.imports.http_resp.Response.set_version = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_version) + wit_world.imports.http_resp.Response.get_status = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.get_status) + wit_world.imports.http_resp.Response.set_status = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_status) + wit_world.imports.http_resp.Response.set_framing_headers_mode = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_framing_headers_mode) + wit_world.imports.http_resp.Response.set_http_keepalive_mode = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.Response.set_http_keepalive_mode) + wit_world.imports.http_resp.send_downstream = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.send_downstream) + wit_world.imports.http_resp.send_downstream_streaming = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.send_downstream_streaming) + wit_world.imports.http_resp.close = remap_wit_errors(MAPPINGS)(wit_world.imports.http_resp.close) + wit_world.imports.http_req.Request.new = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.new) + wit_world.imports.http_req.Request.set_cache_override = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_cache_override) + wit_world.imports.http_req.Request.get_header_names = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_header_names) + wit_world.imports.http_req.Request.get_header_value = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_header_value) + wit_world.imports.http_req.Request.get_header_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_header_values) + wit_world.imports.http_req.Request.set_header_values = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_header_values) + wit_world.imports.http_req.Request.insert_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.insert_header) + wit_world.imports.http_req.Request.append_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.append_header) + wit_world.imports.http_req.Request.remove_header = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.remove_header) + wit_world.imports.http_req.Request.get_method = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_method) + wit_world.imports.http_req.Request.set_method = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_method) + wit_world.imports.http_req.Request.get_uri = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_uri) + wit_world.imports.http_req.Request.set_uri = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_uri) + wit_world.imports.http_req.Request.get_version = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.get_version) + wit_world.imports.http_req.Request.set_version = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_version) + wit_world.imports.http_req.Request.set_auto_decompress_response = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_auto_decompress_response) + wit_world.imports.http_req.Request.redirect_to_websocket_proxy = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.redirect_to_websocket_proxy) + wit_world.imports.http_req.Request.set_framing_headers_mode = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.set_framing_headers_mode) + wit_world.imports.http_req.Request.redirect_to_grip_proxy = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.Request.redirect_to_grip_proxy) + wit_world.imports.http_req.send = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send) + wit_world.imports.http_req.send_uncached = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_uncached) + wit_world.imports.http_req.send_async = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_async) + wit_world.imports.http_req.send_async_uncached = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_async_uncached) + wit_world.imports.http_req.send_async_streaming = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_async_streaming) + wit_world.imports.http_req.send_async_uncached_streaming = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.send_async_uncached_streaming) + wit_world.imports.http_req.await_response = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.await_response) + wit_world.imports.http_req.close = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.close) + wit_world.imports.http_req.upgrade_websocket = remap_wit_errors(MAPPINGS)(wit_world.imports.http_req.upgrade_websocket) + wit_world.imports.http_downstream.next_request = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.next_request) + wit_world.imports.http_downstream.await_request = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.await_request) + wit_world.imports.http_downstream.downstream_original_header_names = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_original_header_names) + wit_world.imports.http_downstream.downstream_original_header_count = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_original_header_count) + wit_world.imports.http_downstream.downstream_client_h2_fingerprint = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_client_h2_fingerprint) + wit_world.imports.http_downstream.downstream_client_request_id = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_client_request_id) + wit_world.imports.http_downstream.downstream_client_oh_fingerprint = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_client_oh_fingerprint) + wit_world.imports.http_downstream.downstream_client_ddos_detected = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_client_ddos_detected) + wit_world.imports.http_downstream.downstream_tls_cipher_openssl_name = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_cipher_openssl_name) + wit_world.imports.http_downstream.downstream_tls_protocol = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_protocol) + wit_world.imports.http_downstream.downstream_tls_client_hello = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_client_hello) + wit_world.imports.http_downstream.downstream_tls_raw_client_certificate = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_raw_client_certificate) + wit_world.imports.http_downstream.downstream_tls_client_cert_verify_result = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_client_cert_verify_result) + wit_world.imports.http_downstream.downstream_tls_client_servername = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_client_servername) + wit_world.imports.http_downstream.downstream_tls_ja3_md5 = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_ja3_md5) + wit_world.imports.http_downstream.downstream_tls_ja4 = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_tls_ja4) + wit_world.imports.http_downstream.downstream_compliance_region = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.downstream_compliance_region) + wit_world.imports.http_downstream.fastly_key_is_valid = remap_wit_errors(MAPPINGS)(wit_world.imports.http_downstream.fastly_key_is_valid) + wit_world.imports.security.inspect = remap_wit_errors(MAPPINGS)(wit_world.imports.security.inspect) + wit_world.imports.kv_store.Store.open = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.open) + wit_world.imports.kv_store.Store.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.lookup) + wit_world.imports.kv_store.Store.lookup_async = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.lookup_async) + wit_world.imports.kv_store.Store.insert = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.insert) + wit_world.imports.kv_store.Store.insert_async = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.insert_async) + wit_world.imports.kv_store.Store.delete = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.delete) + wit_world.imports.kv_store.Store.delete_async = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.delete_async) + wit_world.imports.kv_store.Store.list = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.list) + wit_world.imports.kv_store.Store.list_async = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Store.list_async) + wit_world.imports.kv_store.await_lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.await_lookup) + wit_world.imports.kv_store.await_insert = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.await_insert) + wit_world.imports.kv_store.await_delete = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.await_delete) + wit_world.imports.kv_store.await_list = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.await_list) + wit_world.imports.kv_store.Entry.metadata = remap_wit_errors(MAPPINGS)(wit_world.imports.kv_store.Entry.metadata) + wit_world.imports.acl.Acl.open = remap_wit_errors(MAPPINGS)(wit_world.imports.acl.Acl.open) + wit_world.imports.acl.Acl.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.acl.Acl.lookup) + wit_world.imports.purge.purge_surrogate_key = remap_wit_errors(MAPPINGS)(wit_world.imports.purge.purge_surrogate_key) + wit_world.imports.purge.purge_surrogate_key_verbose = remap_wit_errors(MAPPINGS)(wit_world.imports.purge.purge_surrogate_key_verbose) + wit_world.imports.cache.Entry.lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.lookup) + wit_world.imports.cache.Entry.transaction_lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_lookup) + wit_world.imports.cache.Entry.transaction_lookup_async = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_lookup_async) + wit_world.imports.cache.Entry.transaction_insert = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_insert) + wit_world.imports.cache.Entry.transaction_insert_and_stream_back = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_insert_and_stream_back) + wit_world.imports.cache.Entry.transaction_update = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_update) + wit_world.imports.cache.Entry.get_state = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_state) + wit_world.imports.cache.Entry.get_user_metadata = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_user_metadata) + wit_world.imports.cache.Entry.get_body = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_body) + wit_world.imports.cache.Entry.get_length = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_length) + wit_world.imports.cache.Entry.get_max_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_max_age_ns) + wit_world.imports.cache.Entry.get_stale_while_revalidate_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_stale_while_revalidate_ns) + wit_world.imports.cache.Entry.get_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_age_ns) + wit_world.imports.cache.Entry.get_hits = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.get_hits) + wit_world.imports.cache.Entry.transaction_cancel = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.Entry.transaction_cancel) + wit_world.imports.cache.ReplaceEntry.replace = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.replace) + wit_world.imports.cache.ReplaceEntry.get_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_age_ns) + wit_world.imports.cache.ReplaceEntry.get_body = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_body) + wit_world.imports.cache.ReplaceEntry.get_hits = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_hits) + wit_world.imports.cache.ReplaceEntry.get_length = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_length) + wit_world.imports.cache.ReplaceEntry.get_max_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_max_age_ns) + wit_world.imports.cache.ReplaceEntry.get_stale_while_revalidate_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_stale_while_revalidate_ns) + wit_world.imports.cache.ReplaceEntry.get_state = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_state) + wit_world.imports.cache.ReplaceEntry.get_user_metadata = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.ReplaceEntry.get_user_metadata) + wit_world.imports.cache.insert = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.insert) + wit_world.imports.cache.await_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.await_entry) + wit_world.imports.cache.close_pending_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.close_pending_entry) + wit_world.imports.cache.close_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.close_entry) + wit_world.imports.cache.replace_insert = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.replace_insert) + wit_world.imports.cache.close_replace_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.cache.close_replace_entry) + wit_world.imports.http_cache.Entry.transaction_lookup = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_lookup) + wit_world.imports.http_cache.Entry.transaction_insert = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_insert) + wit_world.imports.http_cache.Entry.transaction_insert_and_stream_back = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_insert_and_stream_back) + wit_world.imports.http_cache.Entry.transaction_update = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_update) + wit_world.imports.http_cache.Entry.transaction_update_and_return_fresh = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_update_and_return_fresh) + wit_world.imports.http_cache.Entry.transaction_record_not_cacheable = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_record_not_cacheable) + wit_world.imports.http_cache.Entry.get_suggested_backend_request = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_suggested_backend_request) + wit_world.imports.http_cache.Entry.get_suggested_write_options = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_suggested_write_options) + wit_world.imports.http_cache.Entry.prepare_response_for_storage = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.prepare_response_for_storage) + wit_world.imports.http_cache.Entry.get_found_response = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_found_response) + wit_world.imports.http_cache.Entry.get_state = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_state) + wit_world.imports.http_cache.Entry.get_length = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_length) + wit_world.imports.http_cache.Entry.get_max_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_max_age_ns) + wit_world.imports.http_cache.Entry.get_stale_while_revalidate_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_stale_while_revalidate_ns) + wit_world.imports.http_cache.Entry.get_age_ns = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_age_ns) + wit_world.imports.http_cache.Entry.get_hits = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_hits) + wit_world.imports.http_cache.Entry.get_sensitive_data = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_sensitive_data) + wit_world.imports.http_cache.Entry.get_surrogate_keys = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_surrogate_keys) + wit_world.imports.http_cache.Entry.get_vary_rule = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.get_vary_rule) + wit_world.imports.http_cache.Entry.transaction_abandon = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.Entry.transaction_abandon) + wit_world.imports.http_cache.is_request_cacheable = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.is_request_cacheable) + wit_world.imports.http_cache.get_suggested_cache_key = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.get_suggested_cache_key) + wit_world.imports.http_cache.close_entry = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.close_entry) + wit_world.imports.http_cache.SuggestedWriteOptions.get_vary_rule = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.SuggestedWriteOptions.get_vary_rule) + wit_world.imports.http_cache.SuggestedWriteOptions.get_surrogate_keys = remap_wit_errors(MAPPINGS)(wit_world.imports.http_cache.SuggestedWriteOptions.get_surrogate_keys) + wit_world.imports.config_store.Store.open = remap_wit_errors(MAPPINGS)(wit_world.imports.config_store.Store.open) + wit_world.imports.config_store.Store.get = remap_wit_errors(MAPPINGS)(wit_world.imports.config_store.Store.get) + wit_world.imports.shielding.shield_info = remap_wit_errors(MAPPINGS)(wit_world.imports.shielding.shield_info) + wit_world.imports.shielding.backend_for_shield = remap_wit_errors(MAPPINGS)(wit_world.imports.shielding.backend_for_shield) + wit_world.imports.image_optimizer.transform_image_optimizer_request = remap_wit_errors(MAPPINGS)(wit_world.imports.image_optimizer.transform_image_optimizer_request) diff --git a/pyproject.toml b/pyproject.toml index 706f3d3..472842c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ ignore = [ "D415", # Don't require punctuation at the end of a docstring summary: sometimes they are noun phrases, not sentences. "D205", # This spuriously complains about line-wrapped single-sentence docstring summaries. Sometimes 80 chars isn't enough. "D107", # Sometimes there's nothing non-obvious to say about a constructor. + "D105", # Usually there's nothing to say about __str__, etc. ] [tool.ruff.format] @@ -63,6 +64,7 @@ quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" +exclude = ["fastly_compute/runtime_patching/patches.py"] [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/scripts/generate_patches/generation.py b/scripts/generate_patches/generation.py index 32abdc0..8cbbc1f 100755 --- a/scripts/generate_patches/generation.py +++ b/scripts/generate_patches/generation.py @@ -38,43 +38,57 @@ def generate_exceptions(error_types: Iterable[Type]): contained code. For example, acl (from the interface name) -> acl_error.py (from the enum name) -> class AclError(FastlyError)... """ - # package name -> module name -> code chunks: - code = defaultdict(lambda: defaultdict(dict)) - packages_to_init = set() + # package -> module -> code: + exceptions = defaultdict(lambda: defaultdict(dict[str, str])) + # package -> module -> docstring: + module_docstrings = defaultdict(dict) + # package -> docstring: + package_docstrings = {} for error_type in error_types: package = error_type.py_package() module = error_type.py_module() + ".py" - # Create package's empty __init__.py if not already there: - packages_to_init.add(package) + try: + module_docstrings[package][module] + except KeyError: + module_docstrings[package][module] = error_type.docstring(indent=0) + + # Create package's __init__.py if not already there: + if package not in package_docstrings: + package_docstrings[package] = error_type.interface().docstring(indent=0) # Common superclass for exceptions based on the enum or variant's # members. Or the raised exception itself for records. top_level_exception_name = error_type.py_exception_name() - code[package][module][top_level_exception_name] = ( + exceptions[package][module][top_level_exception_name] = ( f"""class {top_level_exception_name}(FastlyError):\n""" - f''' """{error_type.docstring_or_pass()}"""\n\n\n''' + f""" {error_type.docstring(indent=4) or "pass"}""" ) # Insert enum or variant cases. for case in error_type.cases(): case_exception_name = case.py_exception_name() - code[package][module][case_exception_name] = ( + exceptions[package][module][case_exception_name] = ( f"""class {case_exception_name}({top_level_exception_name}):\n""" - f''' """{case.docstring_or_pass()}"""\n\n\n''' + f""" {case.docstring(indent=4) or "pass"}""" ) - for package in packages_to_init: + for package, docstring in package_docstrings.items(): write_templated_file( FASTLY_COMPUTE / "exceptions" / package / "__init__.py", - {}, - jinja_env.get_template("empty_init.py.jinja"), + {"module_docstring": docstring}, + jinja_env.get_template("exception_init_module.py.jinja"), ) - for package, modules in code.items(): - for module, exceptions in modules.items(): + for package, modules in exceptions.items(): + for module, exceptions_by_name in modules.items(): write_templated_file( FASTLY_COMPUTE / "exceptions" / package / module, - {"generated_exceptions": partial(join_named_chunks, exceptions)}, + { + "generated_exceptions": partial( + join_named_chunks, exceptions_by_name, "\n\n\n" + ), + "module_docstring": module_docstrings[package][module], + }, jinja_env.get_template("default_exception.py.jinja"), ) @@ -91,11 +105,12 @@ def generate_patches( """ # Collect info: mappings = set() - imports = set() + wit_imports = set() + fastly_imports = set() for error_type in error_types: # Get where it is found in wit_world. Use shallow imports to avoid collisions. - imports.add(error_type.wit_module_path()) - imports.add(error_type.py_module_path()) + wit_imports.add(error_type.wit_module_path()) + fastly_imports.add(error_type.py_module_path()) if error_type.has_cases(): # We need only add the cases; it doesn't make sense in WIT to return # the Enum or Variant itself in a result. @@ -118,7 +133,7 @@ def generate_patches( # Collect import paths for the functions themselves: for func in functions_to_patch: - imports.add(func.wit_module_path()) + wit_imports.add(func.wit_module_path()) # TODO: Maybe automatically improve the docstring of each method to list the # exceptions it raises. @@ -126,7 +141,8 @@ def generate_patches( write_templated_file( FASTLY_COMPUTE / "runtime_patching" / "patches.py", { - "imports": imports, + "fastly_imports": sorted(fastly_imports), + "wit_imports": sorted(wit_imports), "mappings": sorted(mappings), "functions_to_patch": functions_to_patch, }, @@ -134,13 +150,15 @@ def generate_patches( ) -def join_named_chunks(chunks: dict[str, str], omit: list[str] | None = None) -> str: +def join_named_chunks( + chunks: dict[str, str], sep: str, omit: list[str] | None = None +) -> str: """Return an ordered concatenation of all items in a dict except those of the given keys. """ if omit is None: omit = [] - return "".join( + return sep.join( chunk for name, chunk in chunks.items() if name not in omit ) # O(n^2) but small diff --git a/scripts/generate_patches/templates/default_exception.py.jinja b/scripts/generate_patches/templates/default_exception.py.jinja index 738a12d..fa021d4 100644 --- a/scripts/generate_patches/templates/default_exception.py.jinja +++ b/scripts/generate_patches/templates/default_exception.py.jinja @@ -1,5 +1,7 @@ # This file is automatically generated by generate_patches. # It is not intended for manual editing. +{{ module_docstring }} + {% block imports -%} from fastly_compute.exceptions import FastlyError {%- endblock %} diff --git a/scripts/generate_patches/templates/empty_init.py.jinja b/scripts/generate_patches/templates/empty_init.py.jinja deleted file mode 100644 index 8b13789..0000000 --- a/scripts/generate_patches/templates/empty_init.py.jinja +++ /dev/null @@ -1 +0,0 @@ - diff --git a/scripts/generate_patches/templates/exception_init_module.py.jinja b/scripts/generate_patches/templates/exception_init_module.py.jinja new file mode 100644 index 0000000..62eec96 --- /dev/null +++ b/scripts/generate_patches/templates/exception_init_module.py.jinja @@ -0,0 +1,2 @@ +{{ module_docstring }} + diff --git a/scripts/generate_patches/templates/exceptions/types/error.py.jinja b/scripts/generate_patches/templates/exceptions/types/error.py.jinja index aac2a1c..16a6854 100644 --- a/scripts/generate_patches/templates/exceptions/types/error.py.jinja +++ b/scripts/generate_patches/templates/exceptions/types/error.py.jinja @@ -1,9 +1,16 @@ {% extends "default_exception.py.jinja" -%} {% block exceptions -%} -{{ generated_exceptions(omit=["BufferLen"]) -}} +{{ generated_exceptions(omit=["BufferLen"]) }} + class BufferLen(Error): + """Buffer length error + + Returned when a buffer is the wrong size. + Includes the buffer length that would allow the operation to succeed. + """ + def __init__(self, wit_error): self.length = wit_error.value diff --git a/scripts/generate_patches/templates/patches.py.jinja b/scripts/generate_patches/templates/patches.py.jinja index af0d008..f205c5b 100644 --- a/scripts/generate_patches/templates/patches.py.jinja +++ b/scripts/generate_patches/templates/patches.py.jinja @@ -1,17 +1,24 @@ # This file is automatically generated by generate_patches. # It is not intended for manual editing. """Monkeypatches which wrap the routines generated by componentize-py to make -them raise more specific exceptions, not just Err.""" +them raise more specific exceptions, not just Err. +""" try: - from .decorators import remap_wit_errors - {% for import_ in imports -%} + {%- for import_ in wit_imports %} + import {{ import_ }} + {%- endfor %} +{# Skip a line here. #} + {%- for import_ in fastly_imports %} import {{ import_ }} - {% endfor %} + {%- endfor %} + + from .decorators import remap_wit_errors except ImportError: # Tolerate that momentary import for the testrunner before Viceroy, and thus # the wit_world, is around. def patch(): + """Pretend to patch.""" print("Faking the run of exception-mapping monkeypatches for test runner.") else: MAPPINGS = { @@ -25,7 +32,6 @@ else: def patch(): """Apply patches if they haven't already been applied.""" - global did_patch if did_patch: # This test shouldn't be needed, but it avoids double-wrapping the @@ -33,6 +39,7 @@ else: return did_patch = True - {% for func in functions_to_patch -%} + {%- for func in functions_to_patch %} {{ func.wit_path() }} = remap_wit_errors(MAPPINGS)({{ func.wit_path() }}) - {% endfor %} + {%- endfor %} + diff --git a/scripts/generate_patches/utils.py b/scripts/generate_patches/utils.py index 66e9d52..d1c1a92 100644 --- a/scripts/generate_patches/utils.py +++ b/scripts/generate_patches/utils.py @@ -1,7 +1,5 @@ """Little helpers used in patch generation""" -import textwrap - def only(iterable): """Return the one and only item of the iterable, raising ValueError if there @@ -26,13 +24,3 @@ def lower_snake(s: str) -> str: def shouty_snake(s: str) -> str: """Convert lower-kebab case to SHOUTY_SNAKE_CASE.""" return s.replace("-", "_").upper() - - -def indent(s: str): - """Indent as for a docstring. - - Indent all but the first line of a string by 4 spaces, strip leading and - trailing whitespace, and put a newline at the end if there's more than 1 - line. - """ - return textwrap.indent(s, " ").strip() diff --git a/scripts/generate_patches/wit.py b/scripts/generate_patches/wit.py index a452129..f54fe7e 100644 --- a/scripts/generate_patches/wit.py +++ b/scripts/generate_patches/wit.py @@ -10,10 +10,11 @@ import re from collections.abc import Iterable +import textwrap from types import NoneType from typing import Any, Self -from .utils import indent, lower_snake, only, shouty_snake, upper_camel +from .utils import lower_snake, only, shouty_snake, upper_camel class DocsHaver: @@ -25,16 +26,24 @@ class DocsHaver: _me: Any def docs(self) -> str: - """Return the documentation of the type, "" if omitted.""" - return self._me.get("docs", {}).get("contents", "") + """Return the documentation of the type, "" if omitted. - def docstring_or_pass(self) -> str: - """Return a one-level-indented version of the docs suitable for use as a - docstring in an otherwise empty construct. + Strip leading and trailing whitespace. + """ + return self._me.get("docs", {}).get("contents", "").strip() - Accordingly, emit "pass" if there is no docstring. + def docstring(self, indent=4) -> str: + """Return a one-level-indented, triple-quoted version of the docs + suitable for use as a docstring in a top-level construct. """ - return indent(self.docs()) or "pass" + docs = self.docs() + if docs: + if docs.count("\n") > 0: # multi-line + docs += "\n" + docs += '"""' + + return '"""' + textwrap.indent(docs, " " * indent).lstrip() + return "" class Thing(DocsHaver): @@ -308,7 +317,7 @@ def error_type_of_returned_result(self) -> Type | None: return return_type.error_type() -class Interface: +class Interface(DocsHaver): """A WIT interface""" def __init__(self, interface_json: dict[str, Any], wit_json: dict[str, list[dict]]):