Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
endpoint_helpers.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: endpoint_helpers.py
14# CREATION DATE: 11-01-2026
15# LAST Modified: 14:24:16 02-02-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: This file contains helper functions for managing API endpoints.
21# // AR
22# +==== END CatFeeder =================+
23"""
24
25from typing import Union, Dict, List, Any, Tuple, Set
26from string import ascii_letters, digits
27from random import randint
28
29from decimal import Decimal
30from threading import Lock
31from datetime import datetime
32
33from fastapi import Request
34
35from display_tty import Disp, initialise_logger
36from english_words import get_english_words_set
37
38from . import endpoint_constants as EP_CONST
39
40from ..config.env_loader import EnvLoader
41
42EP_IDISP: Disp = initialise_logger(
43 class_name="endpoint_helpers", debug=EnvLoader().debug
44)
45
46# Lock to ensure the english words set is loaded only once across threads
47_WORDS_LOCK = Lock()
48
49
50def datetime_to_string(dt: Union[datetime, str], default_message: str = "<unknown_date>", *, disp: Disp = EP_IDISP) -> str:
51 """Convert a datetime object to ISO 8601 string format.
52
53 Args:
54 dt (datetime): The datetime object to convert.
55
56 Returns:
57 str: The ISO 8601 formatted string representation of the datetime.
58 """
59 disp.log_debug(f"Converting datetime '{dt}' to string.")
60 if isinstance(dt, str):
61 return dt
62 if isinstance(dt, datetime):
63 return dt.isoformat()
64 return default_message
65
66
67def string_to_datetime(date_str: str, default_datetime: datetime = datetime.min, *, disp: Disp = EP_IDISP) -> datetime:
68 """Convert an ISO 8601 string to a datetime object.
69
70 Args:
71 date_str (str): The ISO 8601 formatted string to convert.
72
73 Returns:
74 datetime: The corresponding datetime object.
75 """
76 disp.log_debug(f"Converting string '{date_str}' to datetime.")
77 try:
78 return datetime.fromisoformat(date_str)
79 except (ValueError, TypeError):
80 return default_datetime
81
82
83def convert_datetime_instances_to_strings_list(data: Union[List, Tuple, Set], default_message: str = "<unknown_date>", *, disp: Disp = EP_IDISP, depth: int = 0) -> Union[List[Any], Tuple[Any, ...], Set[Any]]:
84 """Convert all datetime instances in a dictionary to ISO 8601 string format.
85
86 Args:
87 data (dict): The dictionary containing potential datetime instances.
88 default_message (str): The message to use if conversion fails. Defaults to '<unknown_date>'.
89 Returns:
90 dict: The updated dictionary with datetime instances converted to strings.
91 """
92 disp.log_debug("Converting datetime instances in list/tuples to strings.")
93 disp.log_debug(f"Recursion depth: {depth}")
94 disp.log_debug(f"Input data: {data}")
95 final_list = []
96 for key in data:
97 if isinstance(key, datetime):
98 disp.log_debug(
99 f"Converting list/tuple item with datetime value '{key}'."
100 )
101 final_list.append(datetime_to_string(key, default_message))
102 elif isinstance(key, dict):
103 disp.log_debug(
104 "Recursively converting dictionary item in list/tuple."
105 )
106 final_list.append(
108 key, default_message, disp=disp
109 )
110 )
111 elif isinstance(key, (list, tuple, set)):
112 disp.log_debug(
113 "Recursively converting list/tuple item in list/tuple."
114 )
115 final_list.append(
117 key, default_message, disp=disp
118 )
119 )
120 elif isinstance(key, Decimal):
121 disp.log_debug(
122 f"Converting list/tuple item with decimal value '{key}' to float."
123 )
124 final_list.append(convert_decimal_to_float(key, disp=disp))
125 elif isinstance(key, bytes):
126 disp.log_debug(
127 f"Decoding bytes value '{key}' using bytes to str."
128 )
129 final_list.append(convert_bytes_to_str(key, disp=disp))
130 else:
131 final_list.append(key)
132 disp.log_debug(f"Converted data: {final_list}")
133 if isinstance(data, tuple):
134 return tuple(final_list)
135 if isinstance(data, set):
136 return set(final_list)
137 return final_list
138
139
140def convert_datetime_instances_to_strings(data: Dict[str, Any], default_message: str = "<unknown_date>", *, disp: Disp = EP_IDISP, depth: int = 0) -> Dict:
141 """Convert all datetime instances in a dictionary to ISO 8601 string format.
142
143 Args:
144 data (dict): The dictionary containing potential datetime instances.
145 default_message (str): The message to use if conversion fails. Defaults to '<unknown_date>'.
146 Returns:
147 dict: The updated dictionary with datetime instances converted to strings.
148 """
149 disp.log_debug("Converting datetime instances in dictionary to strings.")
150 disp.log_debug(f"Recursion depth: {depth}")
151 disp.log_debug(f"Input data: {data}")
152 for key, value in data.items():
153 if isinstance(value, datetime):
154 disp.log_debug(
155 f"Converting key '{key}' with datetime value '{value}'."
156 )
157 data[key] = datetime_to_string(value, default_message)
158 elif isinstance(value, dict):
159 disp.log_debug(
160 f"Recursively converting dictionary at key '{key}'."
161 )
163 value, default_message, disp=disp, depth=depth + 1
164 )
165 elif isinstance(value, (list, tuple, set)):
166 disp.log_debug(
167 f"Recursively converting list/tuple at key '{key}'."
168 )
170 value, default_message, disp=disp, depth=depth + 1
171 )
172 elif isinstance(value, Decimal):
173 disp.log_debug(
174 f"Converting key '{key}' with decimal value '{value}' to float."
175 )
176 data[key] = convert_decimal_to_float(value, disp=disp)
177 elif isinstance(value, bytes):
178 disp.log_debug(
179 f"Decoding bytes value '{value}' using bytes to str."
180 )
181 data[key] = convert_bytes_to_str(value, disp=disp)
182 else:
183 disp.log_debug(f"No conversion needed for key '{key}'.")
184 data[key] = value
185 disp.log_debug(f"Converted data: {data}")
186 return data
187
188
189def convert_decimal_to_float(data: Decimal, *, disp: Disp = EP_IDISP) -> float:
190 """Convert a decimal.Decimal to float.
191
192 Args:
193 data (decimal.Decimal): The decimal value to convert.
194
195 Returns:
196 float: The converted float value.
197 """
198 disp.log_debug("Converting decimal to float.")
199 try:
200 return float(data)
201 except (ValueError, TypeError) as e:
202 disp.log_debug(f"Failed to convert decimal to float: {e}")
203 return 0.0
204
205
206def convert_float_to_decimal(data: Union[float, int], *, disp: Disp = EP_IDISP) -> Decimal:
207 """Convert a float to decimal.Decimal.
208
209 Args:
210 data (float): The float value to convert.
211
212 Returns:
213 decimal.Decimal: The converted decimal value.
214 """
215 disp.log_debug("Converting float to decimal.")
216 try:
217 return Decimal(data)
218 except (ValueError, TypeError) as e:
219 disp.log_debug(f"Failed to convert float to decimal: {e}")
220 return Decimal(0)
221
222
223def convert_bytes_to_str(data: Union[bytes, str], encoding: str = "utf-8", *, disp: Disp = EP_IDISP) -> str:
224 """Convert bytes to string using the specified encoding.
225
226 Args:
227 data (bytes or str): The data to convert.
228 encoding (str): The encoding to use for conversion. Defaults to 'utf-8'.
229
230 Returns:
231 str: The converted string.
232 """
233 disp.log_debug("Converting bytes to string.")
234 if isinstance(data, bytes):
235 disp.log_debug(f"Decoding bytes data using encoding '{encoding}'.")
236 try:
237 return data.decode(encoding)
238 except (UnicodeDecodeError, AttributeError) as e:
239 disp.log_debug(
240 f"Failed to decode bytes: {e}; returning default string."
241 )
242 return "<undecodable_bytes>"
243 if not isinstance(data, str):
244 disp.log_debug(
245 "Data is neither bytes nor string; converting to string using str()."
246 )
247 return str(data)
248 disp.log_debug("Data is already a string; no conversion needed.")
249 return data
250
251
252def convert_str_to_bytes(data: Union[bytes, str], encoding: str = "utf-8", *, disp: Disp = EP_IDISP) -> bytes:
253 """Convert string to bytes using the specified encoding.
254
255 Args:
256 data (str or bytes): The data to convert.
257 encoding (str): The encoding to use for conversion. Defaults to 'utf-8'.
258
259 Returns:
260 bytes: The converted bytes.
261 """
262 disp.log_debug("Converting string to bytes.")
263 if isinstance(data, str):
264 disp.log_debug(f"Encoding string data using encoding '{encoding}'.")
265 try:
266 return data.encode(encoding)
267 except (UnicodeEncodeError, AttributeError) as e:
268 disp.log_debug(
269 f"Failed to encode string: {e}; returning default bytes."
270 )
271 return b"<un-encodable_string>"
272 if not isinstance(data, bytes):
273 disp.log_debug(
274 "Data is neither string nor bytes; converting to bytes using str()."
275 )
276 return str(data).encode(encoding)
277 disp.log_debug("Data is already bytes; no conversion needed.")
278 return data
279
280
281def load_english_words_tuple_if_required(*, disp: Disp = EP_IDISP) -> Tuple[str, ...]:
282 """Load the set of English words if not already loaded.
283
284 Returns:
285 set: The set of English words.
286 """
287 if EP_CONST.WORDS:
288 disp.log_debug("English words set already loaded.")
289 disp.log_debug(f"Number of words loaded: {EP_CONST.WORDS_LENGTH}")
290 return EP_CONST.WORDS
291
292 disp.log_debug("English words set not loaded yet; acquiring lock to load.")
293 with _WORDS_LOCK:
294 if EP_CONST.WORDS:
295 disp.log_debug("English words set loaded while waiting for lock.")
296 return EP_CONST.WORDS
297 disp.log_debug("Loading English words set.")
298 EP_CONST.WORDS = tuple(get_english_words_set(['web2']))
299 EP_CONST.WORDS_LENGTH = len(EP_CONST.WORDS)
300 disp.log_debug(f"Loaded {EP_CONST.WORDS_LENGTH} English words.")
301 return EP_CONST.WORDS
302
303
304def generate_random_name(word_count: int = 4, *, length: int = 12, link_character: str = "-", disp: Disp = EP_IDISP) -> str:
305 """Generate a random name composed of words or random characters.
306
307 The function attempts to use a cached tuple of English words loaded via
308 :func:`load_english_words_tuple_if_required`. If the word list is empty
309 it falls back to using ASCII letters and digits to build random chunks.
310
311 Args:
312 word_count (int): Number of chunks/words to join. Defaults to 4.
313 length (int): When generating a random-character chunk, the length of that chunk. Defaults to 12.
314 link_character (str): Character used to join chunks. Defaults to '-'.
315 disp (Disp): Logger instance for debug output.
316
317 Returns:
318 str: The generated name composed of `word_count` chunks joined by `link_character`. If falling back to random characters the name is prefixed with an "r" to indicate the fallback mode.
319 """
320 disp.log_debug(f"Generating random name of length {length}.")
321 words_tuple = load_english_words_tuple_if_required(disp=disp)
322 word_length = EP_CONST.WORDS_LENGTH
323 random_letters: bool = False
324 if not words_tuple:
325 disp.log_debug(
326 "English words sequence is empty; returning default name."
327 )
328 words_tuple = tuple(ascii_letters + digits)
329 word_length = len(words_tuple)
330 random_letters = True
331 final_name = []
332 for _attempt in range(word_count):
333 if random_letters:
334 name_chunk: str = ""
335 for _ in range(length):
336 name_chunk += words_tuple[
337 randint(0, word_length - 1)
338 ]
339 final_name.append(name_chunk)
340 else:
341 name_chunk = words_tuple[
342 randint(0, word_length - 1)
343 ]
344 final_name.append(name_chunk)
345 random_name = str(link_character).join(final_name)
346 if random_letters:
347 random_name = f"r{random_name}"
348 disp.log_debug(f"Generated random name: {random_name}")
349 return random_name
350
351
352async def display_request_content(request: Request, *, disp: Disp = EP_IDISP, title: str = "display_request_content") -> None:
353 """Display the content of a FastAPI request for debugging.
354
355 Args:
356 request (Request): The FastAPI request object.
357 disp (Disp): Logger instance for debug output.
358 title (str): Title for logging context.
359 """
360 padding: str = "-" * 42
361 disp.log_debug(padding, title)
362 disp.log_debug("Displaying request content for debugging.", title)
363 disp.log_debug(padding, title)
364 body = await request.body()
365 form = await request.form()
366 disconnected = await request.is_disconnected()
367 disp.log_debug(f"Request body: {body}", title)
368 try:
369 json_body = await request.json()
370 disp.log_debug(f"Request JSON body: {json_body}", title)
371 except Exception as e:
372 disp.log_debug(f"Failed to parse JSON body: {e}", title)
373 disp.log_debug(f"request_content = {request}", title)
374 for index, item in enumerate(request.headers.items()):
375 disp.log_debug(f"header_item[{index}] = {item}", title)
376 disp.log_debug(f"request.app ='{request.app}'", title)
377 disp.log_debug(f"request.url = '{request.url}'", title)
378 # disp.log_debug(f"request.auth = '{request.auth}'") #Only attempt to log if starlette middleware is registered
379 disp.log_debug(f"request.form = '{form}'", title)
380 for index, item in enumerate(request.keys()):
381 disp.log_debug(f"request.keys[{index}] = '{item}'", title)
382 for index, item in enumerate(request.scope.items()):
383 disp.log_debug(f"request.scope[{index}] = '{item}'", title)
384 disp.log_debug(f"request.state = '{request.state}'", title)
385 disp.log_debug(f"request.client = '{request.client}'", title)
386 disp.log_debug(f"request.method = '{request.method}'", title)
387 disp.log_debug(f"request.values = '{request.values()}'", title)
388 for index, item in enumerate(request.cookies.items()):
389 disp.log_debug(f"request.cookies[{index}] = {item}", title)
390 disp.log_debug(f"request.receive = '{request.receive}'", title)
391 # for index, item in enumerate(request.session.items()): #Only attempt to log if starlette middleware is registered
392 # disp.log_debug(f"request.session[{index}] = '{item}'",title)
393 disp.log_debug(f"request.base_url = '{request.base_url}'", title)
394 for index, item in enumerate(request.query_params.items()):
395 disp.log_debug(f"request.query_params[{index}] = {item}", title)
396 disp.log_debug(f"request.is_disconnected = '{disconnected}'", title)
397 for index, item in enumerate(request.path_params.items()):
398 disp.log_debug(f"request.path_params[{index}] = {item}", title)
399 disp.log_debug(padding, title)
400 disp.log_debug("Finished displaying request content.", title)
401 disp.log_debug(padding, title)
402
403
404def sanitize_response_data(data: Union[List[Any], Dict[Any, Any], Tuple[Any, ...], Set[Any]], *, disp: Disp = EP_IDISP) -> Union[List[Any], Dict[Any, Any], Tuple[Any, ...], Set[Any]]:
405 """Sanitize response data by converting datetime instances to strings.
406
407 Args:
408 data (list, dict, or tuple): The response data to sanitize.
409 disp (Disp): Logger instance for debug output.
410
411 Returns:
412 list, dict, or tuple: The sanitized response data.
413 """
414 disp.log_debug("Sanitizing response data.")
415 disp.log_debug("Converting datetime instances in dictionary to strings.")
416 disp.log_debug(f"Input data: {data}")
417 if isinstance(data, dict):
418 return convert_datetime_instances_to_strings(data, disp=disp)
419 if isinstance(data, (list, tuple, set)):
420 return convert_datetime_instances_to_strings_list(data, disp=disp)
421 disp.log_debug(
422 "Data is not iterable, checking if specific conversions are needed."
423 )
424 if isinstance(data, datetime):
425 disp.log_debug(
426 f"Converting datetime value '{data}' to string."
427 )
428 return datetime_to_string(data)
429 if isinstance(data, Decimal):
430 disp.log_debug(
431 f"Converting decimal value '{data}' to float."
432 )
433 return convert_decimal_to_float(data, disp=disp)
434 if isinstance(data, bytes):
435 disp.log_debug("Decoding bytes data using bytes to str.")
436 return convert_bytes_to_str(data, disp=disp)
437 disp.log_debug("No conversion needed for data.")
438 return data
Union[List[Any], Tuple[Any,...], Set[Any]] convert_datetime_instances_to_strings_list(Union[List, Tuple, Set] data, str default_message="<unknown_date>", *, Disp disp=EP_IDISP, int depth=0)
bytes convert_str_to_bytes(Union[bytes, str] data, str encoding="utf-8", *, Disp disp=EP_IDISP)
Union[List[Any], Dict[Any, Any], Tuple[Any,...], Set[Any]] sanitize_response_data(Union[List[Any], Dict[Any, Any], Tuple[Any,...], Set[Any]] data, *, Disp disp=EP_IDISP)
Dict convert_datetime_instances_to_strings(Dict[str, Any] data, str default_message="<unknown_date>", *, Disp disp=EP_IDISP, int depth=0)
str convert_bytes_to_str(Union[bytes, str] data, str encoding="utf-8", *, Disp disp=EP_IDISP)
datetime string_to_datetime(str date_str, datetime default_datetime=datetime.min, *, Disp disp=EP_IDISP)
Decimal convert_float_to_decimal(Union[float, int] data, *, Disp disp=EP_IDISP)
str generate_random_name(int word_count=4, *, int length=12, str link_character="-", Disp disp=EP_IDISP)
None display_request_content(Request request, *, Disp disp=EP_IDISP, str title="display_request_content")
str datetime_to_string(Union[datetime, str] dt, str default_message="<unknown_date>", *, Disp disp=EP_IDISP)
float convert_decimal_to_float(Decimal data, *, Disp disp=EP_IDISP)
Tuple[str,...] load_english_words_tuple_if_required(*, Disp disp=EP_IDISP)