Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
path_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: paths.py
14# CREATION DATE: 11-10-2025
15# LAST Modified: 3:39:15 24-01-2026
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: File in charge of referencing all the paths_initialised supported by the server.
21# // AR
22# +==== END CatFeeder =================+
23"""
24
25from typing import Union, List, Dict, Any, Optional, TYPE_CHECKING, Callable
26import inspect
27
28from display_tty import Disp, initialise_logger
29from .path_constants import PATH_KEY, ENDPOINT_KEY, METHOD_KEY, ALLOWED_METHODS
30from ..core.runtime_manager import RuntimeManager, RI
31from ..core import FinalClass, RuntimeControl
32from .openapi_builder import OpenAPIBuilder
33
34if TYPE_CHECKING:
35 from ..endpoint_manager import EndpointManager
36
37
38class PathManager(metaclass=FinalClass):
39 """Manager for API route registration and validation.
40
41 Handles registration of API endpoints with their paths and HTTP methods,
42 validates route configurations, and manages route injection into FastAPI.
43 Automatically merges methods when the same path/endpoint combination is
44 registered multiple times.
45 """
46
47 disp: Disp = initialise_logger(__qualname__, False)
48
49 def __init__(self, success: int = 0, error: int = 84, debug: bool = False) -> None:
50 """Initialize the path manager.
51
52 Args:
53 success: Success return code.
54 error: Error return code.
55 debug: Enable debug logging.
56 """
57 # ------------------------ The logging function ------------------------
58 self.disp.update_disp_debug(debug)
59 self.disp.log_debug("Initialising...")
60 # -------------------------- Inherited values --------------------------
61 self.success = success
62 self.error = error
63 self.routes: List[Dict[str, Any]] = []
64 self.debug: bool = debug
65 self.runtime_manager_initialised: RuntimeManager = RI
66 # -------------------------- Shared instances --------------------------
67 self.endpoints_initialisedendpoints_initialised: Optional['EndpointManager'] = self.runtime_manager_initialised.get_if_exists(
68 "EndpointManager",
69 None
70 )
72 RuntimeControl)
73 self.openapi_builder = OpenAPIBuilder(debug=debug)
74 self.disp.log_debug("Initialised")
75
76 def endpoint_valid(self, path: str, endpoint: object, method: Union[str, List[str]]) -> bool:
77 """Validate endpoint configuration.
78
79 Checks that path is a string, endpoint is callable, and method(s)
80 are valid HTTP methods from the ALLOWED_METHODS list.
81
82 Args:
83 path: The endpoint path to validate.
84 endpoint: The endpoint function to validate.
85 method: HTTP method(s) to validate.
86
87 Returns:
88 True if all validations pass, False otherwise.
89 """
90 if not isinstance(path, str) or not isinstance(method, (str, list)) or not callable(endpoint):
91 self.disp.log_error(
92 f"Failed to insert {path} with method {method}"
93 )
94 return False
95
96 if isinstance(method, str):
97 methods_to_check = [method]
98 else:
99 methods_to_check = method
100
101 for http_method in methods_to_check:
102 if not isinstance(http_method, str) or http_method.upper() not in ALLOWED_METHODS:
103 self.disp.log_error(
104 f"Failed to insert {path}, method {http_method} not allowed"
105 )
106 return False
107
108 return True
109
110 def _build_endpoint(self, path: str, endpoint: object, method: Union[str, List[str]]) -> Optional[Dict[str, Union[str, object, List[str]]]]:
111 """Build endpoint dictionary from components.
112
113 Validates the endpoint and constructs a standardized dictionary
114 with path, endpoint function, and method(s).
115
116 Args:
117 path: The endpoint path.
118 endpoint: The endpoint function.
119 method: HTTP method(s) - converted to list if string.
120
121 Returns:
122 Dictionary with PATH_KEY, ENDPOINT_KEY, and METHOD_KEY if valid,
123 None if validation fails.
124 """
125 if not self.endpoint_valid(path, endpoint, method):
126 return None
127
128 if isinstance(method, str):
129 methods = [method]
130 else:
131 methods = method
132
133 return {PATH_KEY: path, ENDPOINT_KEY: endpoint, METHOD_KEY: methods}
134
135 def add_path(self, path: str, endpoint: object, method: Union[str, List[str]], *, decorators: Optional[List[Callable]] = None) -> int:
136 """Add or update a path in the routes list."""
137 self.disp.log_debug(f"Adding path <{path}> with methods {method}")
138
139 # Apply decorators if provided
140 if decorators:
141 self.disp.log_debug(
142 f"Applying {len(decorators)} decorator(s) to {path}")
143
144 # Log the original endpoint signature
145 try:
146 orig_sig = inspect.signature(endpoint)
147 self.disp.log_debug(f"Original endpoint signature: {orig_sig}")
148 except Exception as e:
149 self.disp.log_warning(f"Could not get original signature: {e}")
150
151 for i, decorator in enumerate(decorators):
152 decorator_name = getattr(
153 decorator, '__name__', f'decorator_{i}')
154 self.disp.log_debug(
155 f"Applying decorator {decorator_name} to {path}")
156
157 # Check if decorator is callable before applying
158 if not callable(decorator):
159 self.disp.log_error(
160 f"Decorator {decorator_name} is not callable for {path}")
161 return self.error
162
163 # Apply decorator and check if it returns a valid result
164 decorated_endpoint = decorator(endpoint)
165 if not callable(decorated_endpoint):
166 self.disp.log_error(
167 f"Decorator {decorator_name} did not return callable for {path}")
168 return self.error
169
170 # Log the signature after each decorator
171 try:
172 new_sig = inspect.signature(decorated_endpoint)
173 self.disp.log_debug(f"After {decorator_name}: {new_sig}")
174 except Exception as e:
175 self.disp.log_warning(
176 f"Could not get signature after {decorator_name}: {e}")
177
178 endpoint = decorated_endpoint
179 else:
180 self.disp.log_debug(f"No decorators provided for {path}")
181
182 # Validate the endpoint first
183 if not self.endpoint_valid(path, endpoint, method):
184 self.disp.log_error(
185 f"Endpoint validation failed for {path} with method {method}"
186 )
187 return self.error
188
189 # Check if this path + endpoint combination already exists
190 existing_idx = self._find_route_index(path, endpoint)
191
192 if existing_idx != -1:
193 # Route exists - merge methods
194 existing_route = self.routes[existing_idx]
195 merged_methods = self._merge_methods(
196 existing_route[METHOD_KEY],
197 method
198 )
199 self.routes[existing_idx][METHOD_KEY] = merged_methods
200 self.disp.log_warning(
201 f"Updated existing route <{path}> with methods: {merged_methods}"
202 )
203 else:
204 # New route - build and append
205 new_endpoint = self._build_endpoint(path, endpoint, method)
206 if not new_endpoint:
207 self.disp.log_error(f"Failed to build endpoint for {path}")
208 return self.error
209 self.routes.append(new_endpoint)
210 self.disp.log_info(
211 f"Added new route <{path}> with methods: {new_endpoint[METHOD_KEY]}"
212 )
213
214 return self.success
215
216 def has_endpoint(self, path: str, endpoint: object, method: Union[str, List[str]]) -> bool:
217 """Check if an endpoint with exact path, endpoint function, and method exists.
218
219 Args:
220 path: The endpoint path.
221 endpoint: The endpoint function.
222 method: HTTP method(s) to check.
223
224 Returns:
225 True if exact match exists, False otherwise.
226 """
227 endpoint_config = self._build_endpoint(path, endpoint, method)
228 if not endpoint_config:
229 return False
230
231 return endpoint_config in self.routes
232
233 def is_path_registered(self, path: str, method: Optional[Union[str, List[str]]] = None) -> bool:
234 """Check if a path is already registered, optionally with specific method(s).
235
236 Args:
237 path: The endpoint path to check.
238 method: Optional HTTP method(s) to check. If None, checks if path exists with any method.
239
240 Returns:
241 True if path is registered (with optional method match), False otherwise.
242 """
243 for route in self.routes:
244 if route[PATH_KEY] == path:
245 if method is None:
246 # Just checking if path exists, regardless of methods
247 return True
248
249 # Check if specific method(s) are registered
250 if isinstance(method, str):
251 methods_to_check = [method.upper()]
252 else:
253 methods_to_check = [m.upper() for m in method]
254
255 route_methods = [m.upper() for m in route[METHOD_KEY]]
256
257 # Check if any of the requested methods are already registered
258 for check_method in methods_to_check:
259 if check_method in route_methods:
260 return True
261
262 return False
263
264 def add_path_if_not_exists(self, path: str, endpoint: object, method: Union[str, List[str]], *, decorators: Optional[List[Callable]] = None) -> int:
265 """Add path only if it doesn't already exist.
266
267 Args:
268 path: The path to call for the endpoint to be triggered.
269 endpoint: The function that represents the endpoint.
270 method: The HTTP method(s) used (GET, PUT, POST, etc.).
271 decorators: Optional list of decorators to apply.
272
273 Returns:
274 success if it succeeded or already exists, error if there was an error.
275 """
276 if self.is_path_registered(path, method):
277 self.disp.log_debug(
278 f"Path {path} with method(s) {method} already registered, skipping")
279 return self.success
280
281 return self.add_path(path, endpoint, method, decorators=decorators)
282
283 def _find_route_index(self, path: str, endpoint: object) -> int:
284 """Find the index of a route by path and endpoint function.
285
286 Args:
287 path: The endpoint path.
288 endpoint: The endpoint function.
289
290 Returns:
291 Index of the route if found, -1 otherwise.
292 """
293 for idx, route in enumerate(self.routes):
294 if route[PATH_KEY] == path and route[ENDPOINT_KEY] == endpoint:
295 return idx
296 return -1
297
298 def _merge_methods(self, existing_methods: List[str], new_methods: Union[str, List[str]]) -> List[str]:
299 """Merge new methods with existing methods, avoiding duplicates.
300
301 Args:
302 existing_methods: List of existing HTTP methods.
303 new_methods: New method(s) to add.
304
305 Returns:
306 Merged list of unique methods in uppercase.
307 """
308 if isinstance(new_methods, str):
309 methods_to_add = [new_methods]
310 else:
311 methods_to_add = new_methods
312
313 all_methods = set()
314 for method in existing_methods:
315 all_methods.add(method.upper())
316
317 for method in methods_to_add:
318 all_methods.add(method.upper())
319
320 return sorted(all_methods)
321
323 """Load default application paths from EndpointManager.
324
325 Retrieves the EndpointManager instance and calls its inject_routes()
326 method to register all default application endpoints.
327
328 Raises:
329 RuntimeError: If EndpointManager instance cannot be found.
330 """
331 func_title = "load_default_paths_initialised"
332 self.disp.log_debug("Loading default paths_initialised", func_title)
333
335 "EndpointManager",
337 )
338
340 error_message = "EndpointManager could not be found"
341 self.disp.log_critical(error_message)
342 raise RuntimeError(error_message)
343
345
346 def inject_routes(self) -> None:
347 """Inject all registered routes into the FastAPI application."""
348 self.disp.log_info("Starting route injection process")
349 self.disp.log_info(f"Total routes to inject: {len(self.routes)}")
350
351 app = self.runtime_control_initialised.app
352
353 if not app:
354 self.disp.log_critical(
355 "No FastAPI app instance found in RuntimeControl")
356 raise RuntimeError(
357 "No instance was found in the app variable of the RuntimeControl instance")
358
359 if not hasattr(app, "add_api_route"):
360 self.disp.log_critical("FastAPI app missing add_api_route method")
361 raise RuntimeError(
362 "No add_api_route function was found in the app variable of the RuntimeControl instance")
363
364 successful_injections = 0
365 failed_injections = 0
366
367 for i, route in enumerate(self.routes, 1):
368 route_path = route[PATH_KEY]
369 route_methods = route[METHOD_KEY]
370
371 self.disp.log_debug(
372 f"Processing route {i}/{len(self.routes)}: {route_path} [{', '.join(route_methods)}]")
373
374 original_endpoint = route[ENDPOINT_KEY]
375
376 # Let OpenAPIBuilder handle multi-method endpoint preparation
377 endpoint_to_register = self.openapi_builder.prepare_endpoint_for_multi_method(
378 original_endpoint, route_methods)
379
380 if endpoint_to_register != original_endpoint:
381 self.disp.log_debug(
382 f"OpenAPIBuilder created wrapper: {endpoint_to_register.__name__}")
383
384 # Use OpenAPIBuilder to extract metadata
385 route_kwargs = self.openapi_builder.extract_route_metadata(
386 endpoint_to_register)
387 route_kwargs['methods'] = route_methods
388
389 self.disp.log_debug(
390 f"Route metadata for {route_path}: {route_kwargs}")
391
392 try:
393 app.add_api_route(
394 route_path, endpoint_to_register, **route_kwargs)
395 self.disp.log_info(
396 f"Successfully injected route: {route_path} [{', '.join(route_methods)}]")
397 successful_injections += 1
398
399 except Exception as e:
400 self.disp.log_error(f"Error adding route {route_path}: {e}")
401 # Fallback with minimal config
402 try:
403 app.add_api_route(
404 route_path, original_endpoint, methods=route_methods)
405 self.disp.log_warning(
406 f"Fallback injection successful for {route_path}")
407 successful_injections += 1
408 except Exception as fallback_error:
409 self.disp.log_error(
410 f"Fallback injection failed for {route_path}: {fallback_error}")
411 failed_injections += 1
412
413 self.disp.log_info(
414 f"Route injection completed: {successful_injections} successful, {failed_injections} failed")
int add_path_if_not_exists(self, str path, object endpoint, Union[str, List[str]] method, *, Optional[List[Callable]] decorators=None)
int _find_route_index(self, str path, object endpoint)
None __init__(self, int success=0, int error=84, bool debug=False)
bool endpoint_valid(self, str path, object endpoint, Union[str, List[str]] method)
int add_path(self, str path, object endpoint, Union[str, List[str]] method, *, Optional[List[Callable]] decorators=None)
bool has_endpoint(self, str path, object endpoint, Union[str, List[str]] method)
List[str] _merge_methods(self, List[str] existing_methods, Union[str, List[str]] new_methods)
bool is_path_registered(self, str path, Optional[Union[str, List[str]]] method=None)
Optional[Dict[str, Union[str, object, List[str]]]] _build_endpoint(self, str path, object endpoint, Union[str, List[str]] method)