55from abc import abstractmethod
66from collections .abc import Coroutine
77from contextlib import suppress
8+ from functools import cached_property
89from logging import Logger , getLogger
910from typing import Any , ClassVar , TypeVar , final
1011
12+ from diskcache import Cache
13+
1114from hassette .exceptions import CannotOverrideFinalError , FatalError
1215from hassette .types .enums import ResourceRole
1316
@@ -66,6 +69,18 @@ def __init__(cls, name, bases, ns, **kw):
6669class Resource (LifecycleMixin , metaclass = FinalMeta ):
6770 """Base class for resources in the Hassette framework."""
6871
72+ _shutting_down : bool = False
73+ """Flag indicating whether the instance is in the process of shutting down."""
74+
75+ _initializing : bool = False
76+ """Flag indicating whether the instance is in the process of starting up."""
77+
78+ _unique_name : str
79+ """Unique name for the instance."""
80+
81+ _cache : Cache | None
82+ """Private attribute to hold the cache, to allow lazy initialization."""
83+
6984 role : ClassVar [ResourceRole ] = ResourceRole .RESOURCE
7085 """Role of the resource, e.g. 'App', 'Service', etc."""
7186
@@ -78,18 +93,9 @@ class Resource(LifecycleMixin, metaclass=FinalMeta):
7893 children : list ["Resource" ]
7994 """List of child resources."""
8095
81- _shutting_down : bool = False
82- """Flag indicating whether the instance is in the process of shutting down."""
83-
84- _initializing : bool = False
85- """Flag indicating whether the instance is in the process of starting up."""
86-
8796 logger : Logger
8897 """Logger for the instance."""
8998
90- _unique_name : str
91- """Unique name for the instance."""
92-
9399 unique_id : str
94100 """Unique identifier for the instance."""
95101
@@ -124,6 +130,7 @@ def __init__(
124130
125131 super ().__init__ ()
126132
133+ self ._cache = None # lazy init
127134 self .unique_id = uuid .uuid4 ().hex [:8 ]
128135
129136 self .hassette = hassette
@@ -152,6 +159,18 @@ def _setup_logger(self) -> None:
152159 def __repr__ (self ) -> str :
153160 return f"<{ type (self ).__name__ } unique_name={ self .unique_name } >"
154161
162+ @cached_property
163+ def cache (self ) -> Cache :
164+ """Disk cache for storing arbitrary data. All instances of the same resource class share a cache directory."""
165+ if self ._cache is not None :
166+ return self ._cache
167+
168+ # set up cache
169+ cache_dir = self .hassette .config .data_dir .joinpath (self .class_name ).joinpath ("cache" )
170+ cache_dir .mkdir (parents = True , exist_ok = True )
171+ self ._cache = Cache (cache_dir , size_limit = self .hassette .config .default_cache_size )
172+ return self ._cache
173+
155174 @property
156175 def unique_name (self ) -> str :
157176 """Get the unique name of the instance."""
@@ -199,19 +218,6 @@ def add_child(self, child_class: type[_ResourceT], **kwargs) -> _ResourceT:
199218 self .children .append (inst )
200219 return inst
201220
202- # --- developer-facing hooks (override as needed) -------------------
203- async def before_initialize (self ) -> None :
204- """Optional: prepare to accept new work, allocate sockets, queues, temp files, etc."""
205- # Default: nothing. Subclasses override when they own resources.
206-
207- async def on_initialize (self ) -> None :
208- """Primary hook: perform your own initialization (sockets, queues, temp files…)."""
209- # Default: nothing. Subclasses override when they own resources.
210-
211- async def after_initialize (self ) -> None :
212- """Optional: finalize initialization, signal readiness, etc."""
213- # Default: nothing. Subclasses override when they own resources.
214-
215221 @final
216222 async def initialize (self ) -> None :
217223 """Initialize the instance by calling the lifecycle hooks in order."""
@@ -244,19 +250,17 @@ async def initialize(self) -> None:
244250 finally :
245251 self ._initializing = False
246252
247- # --- developer-facing hooks (override as needed) -------------------
248- async def before_shutdown (self ) -> None :
249- """Optional: stop accepting new work, signal loops to wind down, etc."""
250- # Default: cancel an in-flight initialize() task if you used Resource.start()
251- self .cancel ()
253+ async def before_initialize (self ) -> None :
254+ """Optional: prepare to accept new work, allocate sockets, queues, temp files, etc."""
255+ pass
252256
253- async def on_shutdown (self ) -> None :
254- """Primary hook: release your own stuff (sockets, queues, temp files…)."""
255- # Default: nothing. Subclasses override when they own resources.
257+ async def on_initialize (self ) -> None :
258+ """Primary hook: perform your own initialization (sockets, queues, temp files…)."""
259+ pass
256260
257- async def after_shutdown (self ) -> None :
258- """Optional: last-chance actions after on_shutdown, before cleanup/STOPPED ."""
259- # Default: nothing.
261+ async def after_initialize (self ) -> None :
262+ """Optional: finalize initialization, signal readiness, etc ."""
263+ pass
260264
261265 @final
262266 async def shutdown (self ) -> None :
@@ -302,21 +306,45 @@ async def shutdown(self) -> None:
302306
303307 self ._shutting_down = False
304308
309+ async def before_shutdown (self ) -> None :
310+ """Optional: stop accepting new work, signal loops to wind down, etc."""
311+ pass
312+
313+ async def on_shutdown (self ) -> None :
314+ """Primary hook: release your own stuff (sockets, queues, temp files…)."""
315+ pass
316+
317+ async def after_shutdown (self ) -> None :
318+ """Optional: last-chance actions after on_shutdown, before cleanup/STOPPED."""
319+ pass
320+
305321 async def restart (self ) -> None :
306322 """Restart the instance by shutting it down and re-initializing it."""
307323 self .logger .debug ("Restarting '%s' %s" , self .class_name , self .role )
308324 await self .shutdown ()
309325 await self .initialize ()
310326
311- async def cleanup (self ) -> None :
327+ async def cleanup (self , timeout : int | None = None ) -> None :
312328 """Cleanup resources owned by the instance.
313329
314330 This method is called during shutdown to ensure that all resources are properly released.
315331 """
332+ timeout = timeout or self .hassette .config .resource_shutdown_timeout_seconds
333+
316334 self .cancel ()
335+ with suppress (asyncio .CancelledError ):
336+ if self ._init_task :
337+ await asyncio .wait_for (self ._init_task , timeout = timeout )
338+
317339 await self .task_bucket .cancel_all ()
318340 self .logger .debug ("Cleaned up resources" )
319341
342+ if self ._cache is not None :
343+ try :
344+ self .cache .close ()
345+ except Exception as e :
346+ self .logger .exception ("Error closing cache: %s %s" , type (e ).__name__ , e )
347+
320348
321349class Service (Resource ):
322350 """Base class for services in the Hassette framework."""
0 commit comments