diff --git a/fastly_compute/_resource.py b/fastly_compute/_resource.py new file mode 100644 index 0000000..734d327 --- /dev/null +++ b/fastly_compute/_resource.py @@ -0,0 +1,99 @@ +"""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. + """ + + 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). + + The type parameter T represents the underlying WIT binding resource type + and must satisfy the WitResource protocol (context manager support). + """ + + def __init__(self, wit_resource: T): + """Initialize the resource wrapper with an inner WIT binding. + + :param wit_resource: The underlying WIT binding resource to wrap + """ + self._wit_resource = wit_resource + + 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._wit_resource.__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._wit_resource.__exit__(exc_type, exc_val, exc_tb) diff --git a/fastly_compute/config_store.py b/fastly_compute/config_store.py index 335154d..f6ee354 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 @@ -33,10 +35,6 @@ class ConfigStore: api_url = config.get("api_url", "https://api.example.com") """ - def __init__(self, store: wit_config_store.Store): - """Private constructor. Use ConfigStore.open() instead.""" - self._store = store - @classmethod def open(cls, name: str) -> Self: """Open a config store by name. @@ -67,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._store.get(key, _MAX_U32) + result = self._wit_resource.get(key, _MAX_U32) if result is None: result = default @@ -90,31 +88,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..f6f34f7 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 @@ -42,10 +44,6 @@ class LogEndpoint: endpoint.write(b"Binary log data") """ - def __init__(self, endpoint: wit_log.Endpoint): - """Private constructor. Use LogEndpoint.open() instead.""" - self._endpoint = endpoint - @classmethod def open(cls, name: str) -> Self: r"""Open a logging endpoint by name. @@ -88,35 +86,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._wit_resource.write(msg) class FastlyLogHandler(logging.Handler):