Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
runtime_manager.py
Go to the documentation of this file.
1r"""
2# +==== BEGIN CatFeeder =================+
3# LOGO:
4# ..............(..../\
5# ...............)..(.')
6# ..............(../..)
7# ...............\‍(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
10# animals/cats
11# /STOP
12# PROJECT: CatFeeder
13# FILE: runtime_manager.py
14# CREATION DATE: 22-11-2025
15# LAST Modified: 14:43:14 19-12-2025
16# DESCRIPTION:
17# This is the backend server in charge of making the actual website work.
18# /STOP
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: Provide an ECS-like lookup dictionary with lazy singleton creation.
21# // AR
22# +==== END CatFeeder =================+
23"""
24from __future__ import annotations
25from typing import Type, TypeVar, Dict, Any, Union, overload, Optional
26import asyncio
27import threading
28
29from display_tty import Disp, initialise_logger
30
31from .final_singleton_class import FinalSingleton
32
33T = TypeVar("T") # legacy / compatibility if external code refers
34S = TypeVar("S") # precise service type for overloads
35
36
38 """Flexible runtime service container.
39
40 Summary:
41 Lazy, thread-safe instantiation and retrieval of service classes.
42 Supports plain classes or ``FinalSingleton`` subclasses. First access
43 may supply constructor args; later accesses ignore them.
44
45 Raises:
46 RuntimeError: Named lookup with no registration.
47 TypeError: Constructor argument mismatch (propagated).
48 Exception: Any user exception from constructor or ``async_init``.
49
50 Notes:
51 Failed constructions are not cached; later calls retry.
52 """
53
54 _instances: Dict[str, Any] = {}
55 _classes: Dict[str, Type[Any]] = {}
56 _thread_locks: Dict[str, threading.Lock] = {}
57
58 _disp: Disp = initialise_logger(__qualname__, False)
59
60 @classmethod
61 def update_debug(cls, debug: bool = False) -> None:
62 cls._disp.update_disp_debug(debug)
63
64 # ------------------------------------------------------
65 # SETTER
66 # ------------------------------------------------------
67 @classmethod
68 def set(cls, service: Type[T], *args, **kwargs) -> None:
69 """Register and construct eagerly.
70
71 No-op if already present.
72 Raises any exception from constructor / async_init.
73 """
74 key = service.__name__
75 tlock = cls._thread_locks.setdefault(key, threading.Lock())
76
77 with tlock:
78 if key in cls._instances:
79 # Already initialized
80 return
81
82 cls._classes[key] = service
83
84 # Handle FinalSingleton enforcement
85 if issubclass(service, FinalSingleton):
86 setattr(service, "_allow_create", True)
87 instance = service(*args, **kwargs)
88 setattr(service, "_allow_create", False)
89 else:
90 instance = service(*args, **kwargs)
91
92 # Async initializer
93 async_init = getattr(instance, "async_init", None)
94 if callable(async_init):
95 maybe_coro = async_init()
96 if asyncio.iscoroutine(maybe_coro):
97 asyncio.run(maybe_coro)
98
99 cls._instances[key] = instance
100
101 # ------------------------------------------------------
102 # GETTER
103 # ------------------------------------------------------
104 @overload
105 @classmethod
106 def get(cls, service: Type[S], *args,
107 auto_register: bool = False, **kwargs) -> S: ...
108
109 @overload
110 @classmethod
111 def get(cls, service: str, *args,
112 auto_register: bool = False, **kwargs) -> Any: ...
113
114 @classmethod
115 def get(cls, service: Union[str, Type[S]], *args, auto_register: bool = False, **kwargs) -> Any:
116 """Retrieve (and lazily initialize) a service instance.
117
118 Overloads:
119 - ``get(Type[Service]) -> Service``
120 - ``get("ServiceName") -> Any`` (returns registered instance when the concrete type is not statically known)
121
122 First call may pass constructor args/kwargs; they are ignored on later calls.
123 Async initialization via optional ``async_init`` coroutine is supported.
124
125 Args:
126 service: Class type or string name of the service.
127 *args: Constructor arguments for first initialization.
128 auto_register: If True, automatically register unregistered classes. If False, raise RuntimeError. Defaults to True.
129 **kwargs: Constructor keyword arguments for first initialization.
130
131 Raises
132 RuntimeError if named lookup missing or auto_register=False with unregistered class; propagates constructor / async_init exceptions.
133 """
134 if isinstance(service, str):
135 key = service
136 klass = cls._classes.get(key)
137 if klass is None:
138 raise RuntimeError(
139 f"Class {key} not registered in RuntimeManager."
140 )
141 else:
142 key = service.__name__
143 klass = service
144 if key not in cls._classes:
145 if not auto_register:
146 raise RuntimeError(
147 f"Class {key} not registered in RuntimeManager. Use set() first or pass auto_register=True."
148 )
149 cls._classes[key] = klass
150
151 existing = cls._instances.get(key)
152 if existing is not None:
153 return existing
154
155 tlock = cls._thread_locks.setdefault(key, threading.Lock())
156 with tlock:
157 existing = cls._instances.get(key)
158 if existing is not None:
159 return existing
160
161 # FinalSingleton enforcement
162 if issubclass(klass, FinalSingleton):
163 setattr(klass, "_allow_create", True)
164 instance = klass(*args, **kwargs)
165 setattr(klass, "_allow_create", False)
166 else:
167 instance = klass(*args, **kwargs)
168
169 async_init = getattr(instance, "async_init", None)
170 if callable(async_init):
171 maybe_coro = async_init()
172 if asyncio.iscoroutine(maybe_coro):
173 asyncio.run(maybe_coro)
174
175 cls._instances[key] = instance
176 return instance
177
178 # ------------------------------------------------------
179 # GET IF EXISTS
180 # ------------------------------------------------------
181
182 @overload
183 @classmethod
184 def get_if_exists(cls, service: Type[S],
185 target: Optional[object] = None) -> Optional[S]: ...
186
187 @overload
188 @classmethod
189 def get_if_exists(cls, service: str,
190 target: Optional[object] = None) -> Optional[Any]: ...
191
192 @classmethod
193 def get_if_exists(cls, service: Union[str, Type[S]], target: Optional[object] = None) -> Optional[Any]:
194 class_name = getattr(service, "__qualname__", None) or getattr(
195 service, "__name__", "")
196 cls._disp.log_debug(f"Attempting to retrieve the {class_name} class")
197 if target:
198 cls._disp.log_debug(
199 f"{class_name} class exists and is stored in class, returning"
200 )
201 return target
202 if cls.exists(service):
203 cls._disp.log_debug(f"{class_name} class exists, retrieving")
204 return cls.getgetget(service)
205
206 cls._disp.log_debug(
207 f"{class_name} class does not exist, returning None"
208 )
209 return None
210
211 # ------------------------------------------------------
212 # CHECK IF INITIALIZED
213 # ------------------------------------------------------
214
215 @classmethod
216 def exists(cls, service: Union[str, Type[T]]) -> bool:
217 """Return True if service instance is already initialized."""
218 if isinstance(service, str):
219 key = service
220 else:
221 if hasattr(service, "__name__"):
222 key = service.__name__
223 elif hasattr(service, "__qualname__"):
224 key = service.__qualname__
225 else:
226 return False
227 return key in cls._instances
228
229
230# Singleton instance to make sure a shared instance exists
231RI: RuntimeManager = RuntimeManager()
S get(cls, Type[S] service, *args, bool auto_register=False, **kwargs)
Any get(cls, str service, *args, bool auto_register=False, **kwargs)
Any get(cls, Union[str, Type[S]] service, *args, bool auto_register=False, **kwargs)
None set(cls, Type[T] service, *args, **kwargs)
Optional[S] get_if_exists(cls, Type[S] service, Optional[object] target=None)
bool exists(cls, Union[str, Type[T]] service)