2# +==== BEGIN CatFeeder =================+
5# ...............)..(.')
7# ...............\(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
13# FILE: endpoint_helpers.py
14# CREATION DATE: 11-01-2026
15# LAST Modified: 14:24:16 02-02-2026
17# This is the backend server in charge of making the actual website work.
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: This file contains helper functions for managing API endpoints.
22# +==== END CatFeeder =================+
25from typing
import Union, Dict, List, Any, Tuple, Set
26from string
import ascii_letters, digits
27from random
import randint
29from decimal
import Decimal
30from threading
import Lock
31from datetime
import datetime
33from fastapi
import Request
35from display_tty
import Disp, initialise_logger
36from english_words
import get_english_words_set
38from .
import endpoint_constants
as EP_CONST
40from ..config.env_loader
import EnvLoader
42EP_IDISP: Disp = initialise_logger(
43 class_name=
"endpoint_helpers", debug=
EnvLoader().debug
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.
54 dt (datetime): The datetime object to convert.
57 str: The ISO 8601 formatted string representation of the datetime.
59 disp.log_debug(f
"Converting datetime '{dt}' to string.")
60 if isinstance(dt, str):
62 if isinstance(dt, datetime):
64 return default_message
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.
71 date_str (str): The ISO 8601 formatted string to convert.
74 datetime: The corresponding datetime object.
76 disp.log_debug(f
"Converting string '{date_str}' to datetime.")
78 return datetime.fromisoformat(date_str)
79 except (ValueError, TypeError):
80 return default_datetime
84 """Convert all datetime instances in a dictionary to ISO 8601 string format.
87 data (dict): The dictionary containing potential datetime instances.
88 default_message (str): The message to use if conversion fails. Defaults to '<unknown_date>'.
90 dict: The updated dictionary with datetime instances converted to strings.
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}")
97 if isinstance(key, datetime):
99 f
"Converting list/tuple item with datetime value '{key}'."
101 final_list.append(datetime_to_string(key, default_message))
102 elif isinstance(key, dict):
104 "Recursively converting dictionary item in list/tuple."
108 key, default_message, disp=disp
111 elif isinstance(key, (list, tuple, set)):
113 "Recursively converting list/tuple item in list/tuple."
117 key, default_message, disp=disp
120 elif isinstance(key, Decimal):
122 f
"Converting list/tuple item with decimal value '{key}' to float."
125 elif isinstance(key, bytes):
127 f
"Decoding bytes value '{key}' using bytes to str."
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)
141 """Convert all datetime instances in a dictionary to ISO 8601 string format.
144 data (dict): The dictionary containing potential datetime instances.
145 default_message (str): The message to use if conversion fails. Defaults to '<unknown_date>'.
147 dict: The updated dictionary with datetime instances converted to strings.
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):
155 f
"Converting key '{key}' with datetime value '{value}'."
157 data[key] = datetime_to_string(value, default_message)
158 elif isinstance(value, dict):
160 f
"Recursively converting dictionary at key '{key}'."
163 value, default_message, disp=disp, depth=depth + 1
165 elif isinstance(value, (list, tuple, set)):
167 f
"Recursively converting list/tuple at key '{key}'."
170 value, default_message, disp=disp, depth=depth + 1
172 elif isinstance(value, Decimal):
174 f
"Converting key '{key}' with decimal value '{value}' to float."
177 elif isinstance(value, bytes):
179 f
"Decoding bytes value '{value}' using bytes to str."
183 disp.log_debug(f
"No conversion needed for key '{key}'.")
185 disp.log_debug(f
"Converted data: {data}")
190 """Convert a decimal.Decimal to float.
193 data (decimal.Decimal): The decimal value to convert.
196 float: The converted float value.
198 disp.log_debug(
"Converting decimal to float.")
201 except (ValueError, TypeError)
as e:
202 disp.log_debug(f
"Failed to convert decimal to float: {e}")
207 """Convert a float to decimal.Decimal.
210 data (float): The float value to convert.
213 decimal.Decimal: The converted decimal value.
215 disp.log_debug(
"Converting float to decimal.")
218 except (ValueError, TypeError)
as e:
219 disp.log_debug(f
"Failed to convert float to decimal: {e}")
224 """Convert bytes to string using the specified encoding.
227 data (bytes or str): The data to convert.
228 encoding (str): The encoding to use for conversion. Defaults to 'utf-8'.
231 str: The converted string.
233 disp.log_debug(
"Converting bytes to string.")
234 if isinstance(data, bytes):
235 disp.log_debug(f
"Decoding bytes data using encoding '{encoding}'.")
237 return data.decode(encoding)
238 except (UnicodeDecodeError, AttributeError)
as e:
240 f
"Failed to decode bytes: {e}; returning default string."
242 return "<undecodable_bytes>"
243 if not isinstance(data, str):
245 "Data is neither bytes nor string; converting to string using str()."
248 disp.log_debug(
"Data is already a string; no conversion needed.")
253 """Convert string to bytes using the specified encoding.
256 data (str or bytes): The data to convert.
257 encoding (str): The encoding to use for conversion. Defaults to 'utf-8'.
260 bytes: The converted bytes.
262 disp.log_debug(
"Converting string to bytes.")
263 if isinstance(data, str):
264 disp.log_debug(f
"Encoding string data using encoding '{encoding}'.")
266 return data.encode(encoding)
267 except (UnicodeEncodeError, AttributeError)
as e:
269 f
"Failed to encode string: {e}; returning default bytes."
271 return b
"<un-encodable_string>"
272 if not isinstance(data, bytes):
274 "Data is neither string nor bytes; converting to bytes using str()."
276 return str(data).encode(encoding)
277 disp.log_debug(
"Data is already bytes; no conversion needed.")
282 """Load the set of English words if not already loaded.
285 set: The set of English 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
292 disp.log_debug(
"English words set not loaded yet; acquiring lock to load.")
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
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.
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.
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.
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.
320 disp.log_debug(f
"Generating random name of length {length}.")
322 word_length = EP_CONST.WORDS_LENGTH
323 random_letters: bool =
False
326 "English words sequence is empty; returning default name."
328 words_tuple = tuple(ascii_letters + digits)
329 word_length = len(words_tuple)
330 random_letters =
True
332 for _attempt
in range(word_count):
335 for _
in range(length):
336 name_chunk += words_tuple[
337 randint(0, word_length - 1)
339 final_name.append(name_chunk)
341 name_chunk = words_tuple[
342 randint(0, word_length - 1)
344 final_name.append(name_chunk)
345 random_name = str(link_character).join(final_name)
347 random_name = f
"r{random_name}"
348 disp.log_debug(f
"Generated random name: {random_name}")
353 """Display the content of a FastAPI request for debugging.
356 request (Request): The FastAPI request object.
357 disp (Disp): Logger instance for debug output.
358 title (str): Title for logging context.
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)
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)
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)
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)
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.
408 data (list, dict, or tuple): The response data to sanitize.
409 disp (Disp): Logger instance for debug output.
412 list, dict, or tuple: The sanitized response data.
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):
419 if isinstance(data, (list, tuple, set)):
422 "Data is not iterable, checking if specific conversions are needed."
424 if isinstance(data, datetime):
426 f
"Converting datetime value '{data}' to string."
428 return datetime_to_string(data)
429 if isinstance(data, Decimal):
431 f
"Converting decimal value '{data}' to float."
434 if isinstance(data, bytes):
435 disp.log_debug(
"Decoding bytes data using bytes to str.")
437 disp.log_debug(
"No conversion needed for 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)