From 3bcdc2a4e8e7252bcb4c350e590e09727cf87773 Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Thu, 19 Feb 2026 12:53:07 -0600 Subject: [PATCH 1/2] Add FastlyResource base class for common concerns Much of the API functinoality is wrapping and providing documentation and some basic translation on top of an underlying (patched) wit resource. With that wrapping, however, we need to consistently add context manager and close() support. This change moves that common functionality to an abstract base class with generic types so we continue to have strong typing in our implementations interactions with the "_inner" WIT resource. --- fastly_compute/config_store.py | 36 ++---------- fastly_compute/log.py | 36 ++---------- fastly_compute/resource.py | 104 +++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 fastly_compute/resource.py diff --git a/fastly_compute/config_store.py b/fastly_compute/config_store.py index 335154d..91d3500 100644 --- a/fastly_compute/config_store.py +++ b/fastly_compute/config_store.py @@ -15,13 +15,15 @@ from wit_world.imports import config_store as wit_config_store +from .resource import FastlyResource + # The maximum value for a u32, used to signal that we don't want to cap # the length of values returned by the host. In practice, this limit # is at 8KB, though that could change. _MAX_U32 = 0xFFFFFFFF -class ConfigStore: +class ConfigStore(FastlyResource[wit_config_store.Store]): """Interface to Fastly Config Store. Config Stores provide read-only access to configuration data that can be @@ -35,7 +37,7 @@ class ConfigStore: def __init__(self, store: wit_config_store.Store): """Private constructor. Use ConfigStore.open() instead.""" - self._store = store + super().__init__(store) @classmethod def open(cls, name: str) -> Self: @@ -67,7 +69,7 @@ def get(self, key: str, default: str | None = None) -> str | None: config = ConfigStore.open("app-config") api_url = config.get("api_url", "https://api.example.com") """ - result = self._store.get(key, _MAX_U32) + result = self._inner.get(key, _MAX_U32) if result is None: result = default @@ -90,31 +92,3 @@ def __contains__(self, key: str) -> bool: if not isinstance(key, str): raise KeyError("Key must be a str") return self.get(key) is not None - - def close(self) -> None: - """Explicitly close the config store, releasing its resources. - - This is called automatically when using the config store as a context - manager. If not called explicitly, resources will eventually be freed - by the garbage collector. - - .. note:: Attempting to use the config store after it is closed will result - in a trap. - """ - self._store.__exit__(None, None, None) - - def __enter__(self) -> Self: - """Context manager entry. - - Allows use of ConfigStore in a 'with' statement. - """ - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Context manager exit. - - Use of the context manager will free up the underlying host resource on - exit. Referencing the resource after context manager exit will result in - a trap. - """ - self.close() diff --git a/fastly_compute/log.py b/fastly_compute/log.py index e4f9bc2..d5cdbe0 100644 --- a/fastly_compute/log.py +++ b/fastly_compute/log.py @@ -28,8 +28,10 @@ from wit_world.imports import log as wit_log +from .resource import FastlyResource -class LogEndpoint: + +class LogEndpoint(FastlyResource[wit_log.Endpoint]): """Interface to a Fastly logging endpoint. Logging endpoints send log data to configured Real-Time Log Streaming @@ -44,7 +46,7 @@ class LogEndpoint: def __init__(self, endpoint: wit_log.Endpoint): """Private constructor. Use LogEndpoint.open() instead.""" - self._endpoint = endpoint + super().__init__(endpoint) @classmethod def open(cls, name: str) -> Self: @@ -88,35 +90,7 @@ def write(self, msg: bytes | str) -> None: """ if isinstance(msg, str): msg = msg.encode("utf-8") - self._endpoint.write(msg) - - def close(self) -> None: - """Explicitly close the logging endpoint, releasing its resources. - - This is called automatically when using the endpoint as a context - manager. If not called explicitly, resources will eventually be freed - by the garbage collector. - - Note: Attempting to use the endpoint after it is closed will result - in a trap. - """ - self._endpoint.__exit__(None, None, None) - - def __enter__(self) -> Self: - """Context manager entry. - - Allows use of resource in a 'with' statement. - """ - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Context manager exit. - - Use of the context manager will free up the underlying host resource on - exit. Referencing the resource after context manager exit will result in - a trap. - """ - self._endpoint.__exit__(exc_type, exc_val, exc_tb) + self._inner.write(msg) class FastlyLogHandler(logging.Handler): diff --git a/fastly_compute/resource.py b/fastly_compute/resource.py new file mode 100644 index 0000000..22ea208 --- /dev/null +++ b/fastly_compute/resource.py @@ -0,0 +1,104 @@ +"""Internal base class for Fastly resource wrappers + +This module provides an internal generic base class for wrapping WIT binding +resources with consistent lifecycle management and context manager protocol. + +**Note**: This module is for internal SDK use only. End users should not +need to import or use these classes directly. Instead, use the public resource +classes like ConfigStore, RateCounter, PenaltyBox, and LogEndpoint. +""" + +from __future__ import annotations + +from types import TracebackType +from typing import Protocol, Self + + +class WitResource(Protocol): + """Internal protocol for WIT-generated resource types. + + This protocol defines the context manager interface that all WIT resources + must implement for resource lifecycle management. + + **Internal use only** - do not use directly. + """ + + def __enter__(self) -> Self: + """Enter the context manager.""" + ... + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> bool | None: + """Exit the context manager and release resources.""" + ... + + +class FastlyResource[T: WitResource]: + """Internal base class for Fastly resource wrappers. + + This generic base class provides consistent context manager protocol and + resource lifecycle management for all Fastly resource types that wrap + WIT bindings (e.g., ConfigStore, RateCounter, PenaltyBox, LogEndpoint). + + **Internal use only** - do not instantiate or subclass directly. Use the + public resource classes instead (ConfigStore, RateCounter, etc.). + + The type parameter T represents the underlying WIT binding resource type + and must satisfy the WitResource protocol (context manager support). + """ + + def __init__(self, inner: T): + """Initialize the resource wrapper with an inner WIT binding. + + :param inner: The underlying WIT binding resource to wrap + """ + self._inner = inner + + def close(self) -> None: + """Explicitly close the resource, releasing its resources. + + This is called automatically when using the resource as a context + manager. If not called explicitly, resources will eventually be freed + by the garbage collector. + + Note: Attempting to use the resource after it is closed will result + in a trap. + """ + self._inner.__exit__(None, None, None) + + def __enter__(self) -> Self: + """Context manager entry. + + Allows use of the resource in a 'with' statement. + + Example:: + + with Resource.open("foo") as foo: + value = foo.bar("baz") + """ + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Context manager exit. + + Use of the context manager will free up the underlying host resource on + exit. Referencing the resource after context manager exit will result in + a trap. + + Exception information from the context is passed through to the inner + resource's __exit__ method for proper cleanup. + + :param exc_type: Exception type if an exception occurred, None otherwise + :param exc_val: Exception value if an exception occurred, None otherwise + :param exc_tb: Exception traceback if an exception occurred, None otherwise + """ + self._inner.__exit__(exc_type, exc_val, exc_tb) From 572fc68968a865990416c5195dfd01b094e659ad Mon Sep 17 00:00:00 2001 From: Paul Osborne Date: Tue, 7 Apr 2026 16:57:32 -0500 Subject: [PATCH 2/2] Address review feedback on resource base class - Remove unecessary constructor wrapper calls - Rename _inner to _wit_resource - Rename module to _resource from resource as private, remove some of the internal disclaimers. --- fastly_compute/{resource.py => _resource.py} | 15 +++++---------- fastly_compute/config_store.py | 8 ++------ fastly_compute/log.py | 8 ++------ 3 files changed, 9 insertions(+), 22 deletions(-) rename fastly_compute/{resource.py => _resource.py} (87%) diff --git a/fastly_compute/resource.py b/fastly_compute/_resource.py similarity index 87% rename from fastly_compute/resource.py rename to fastly_compute/_resource.py index 22ea208..734d327 100644 --- a/fastly_compute/resource.py +++ b/fastly_compute/_resource.py @@ -19,8 +19,6 @@ class WitResource(Protocol): This protocol defines the context manager interface that all WIT resources must implement for resource lifecycle management. - - **Internal use only** - do not use directly. """ def __enter__(self) -> Self: @@ -44,19 +42,16 @@ class FastlyResource[T: WitResource]: resource lifecycle management for all Fastly resource types that wrap WIT bindings (e.g., ConfigStore, RateCounter, PenaltyBox, LogEndpoint). - **Internal use only** - do not instantiate or subclass directly. Use the - public resource classes instead (ConfigStore, RateCounter, etc.). - The type parameter T represents the underlying WIT binding resource type and must satisfy the WitResource protocol (context manager support). """ - def __init__(self, inner: T): + def __init__(self, wit_resource: T): """Initialize the resource wrapper with an inner WIT binding. - :param inner: The underlying WIT binding resource to wrap + :param wit_resource: The underlying WIT binding resource to wrap """ - self._inner = inner + self._wit_resource = wit_resource def close(self) -> None: """Explicitly close the resource, releasing its resources. @@ -68,7 +63,7 @@ def close(self) -> None: Note: Attempting to use the resource after it is closed will result in a trap. """ - self._inner.__exit__(None, None, None) + self._wit_resource.__exit__(None, None, None) def __enter__(self) -> Self: """Context manager entry. @@ -101,4 +96,4 @@ def __exit__( :param exc_val: Exception value if an exception occurred, None otherwise :param exc_tb: Exception traceback if an exception occurred, None otherwise """ - self._inner.__exit__(exc_type, exc_val, exc_tb) + self._wit_resource.__exit__(exc_type, exc_val, exc_tb) diff --git a/fastly_compute/config_store.py b/fastly_compute/config_store.py index 91d3500..f6ee354 100644 --- a/fastly_compute/config_store.py +++ b/fastly_compute/config_store.py @@ -15,7 +15,7 @@ from wit_world.imports import config_store as wit_config_store -from .resource import FastlyResource +from ._resource import FastlyResource # The maximum value for a u32, used to signal that we don't want to cap # the length of values returned by the host. In practice, this limit @@ -35,10 +35,6 @@ class ConfigStore(FastlyResource[wit_config_store.Store]): api_url = config.get("api_url", "https://api.example.com") """ - def __init__(self, store: wit_config_store.Store): - """Private constructor. Use ConfigStore.open() instead.""" - super().__init__(store) - @classmethod def open(cls, name: str) -> Self: """Open a config store by name. @@ -69,7 +65,7 @@ def get(self, key: str, default: str | None = None) -> str | None: config = ConfigStore.open("app-config") api_url = config.get("api_url", "https://api.example.com") """ - result = self._inner.get(key, _MAX_U32) + result = self._wit_resource.get(key, _MAX_U32) if result is None: result = default diff --git a/fastly_compute/log.py b/fastly_compute/log.py index d5cdbe0..f6f34f7 100644 --- a/fastly_compute/log.py +++ b/fastly_compute/log.py @@ -28,7 +28,7 @@ from wit_world.imports import log as wit_log -from .resource import FastlyResource +from ._resource import FastlyResource class LogEndpoint(FastlyResource[wit_log.Endpoint]): @@ -44,10 +44,6 @@ class LogEndpoint(FastlyResource[wit_log.Endpoint]): endpoint.write(b"Binary log data") """ - def __init__(self, endpoint: wit_log.Endpoint): - """Private constructor. Use LogEndpoint.open() instead.""" - super().__init__(endpoint) - @classmethod def open(cls, name: str) -> Self: r"""Open a logging endpoint by name. @@ -90,7 +86,7 @@ def write(self, msg: bytes | str) -> None: """ if isinstance(msg, str): msg = msg.encode("utf-8") - self._inner.write(msg) + self._wit_resource.write(msg) class FastlyLogHandler(logging.Handler):