2# +==== BEGIN CatFeeder =================+
5# ...............)..(.')
7# ...............\(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
13# FILE: favicon_admin.py
14# CREATION DATE: 05-01-2026
15# LAST Modified: 1:39:39 13-01-2026
17# This is the backend server in charge of making the actual website work.
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: This is the handler for the admin favicons.
22# +==== END CatFeeder =================+
25from typing
import TYPE_CHECKING, Optional, List, Union, Dict, Any
27from fastapi
import Response, UploadFile
28from starlette.datastructures
import UploadFile
as StarletteUploadFile
29from display_tty
import Disp, initialise_logger
31from .
import favicon_constants
as FAV_CONST
32from .
import favicon_helpers
as FAV_HELPERS
33from .
import favicon_error_class
as FAV_ERR
35from ..core
import FinalSingleton
36from ..core.runtime_manager
import RI, RuntimeManager
37from ..image_reducer
import FileFormat
38from ..http_codes
import HCI, HttpDataTypes
39from ..utils
import CONST
43 from ..bucket
import Bucket
44 from ..image_reducer
import ImageReducer
45 from ..server_header
import ServerHeaders
46 from ..boilerplates.responses
import BoilerplateResponses
56 disp: Disp = initialise_logger(__qualname__,
False)
62 def __init__(self, success: int = 0, error: int = 84, debug: bool =
False) ->
None:
63 """Initialize the ImageReducer instance with dependencies.
66 debug: Enable debug logging when True. Defaults to False.
69 self.
disp.update_disp_debug(debug)
72 self.
disp.log_debug(
"Initialising...")
75 "BoilerplateResponses")
82 self.
disp.log_debug(
"Initialised")
84 def _no_user_id(self, title: str, token: Optional[str] =
None) -> Response:
85 """Return a standardized response when no user id is provided.
87 Logs an error and returns the `user_not_found` response constructed by
88 the configured boilerplate response helper.
91 title (str): Title to include in the response context.
92 token (Optional[str]): Optional token related to the request.
95 Response: A FastAPI response produced by `BoilerplateResponses`.
97 self.
disp.log_error(
"There is no specified user id.")
100 def _no_data(self, title: str, token: Optional[str] =
None) -> Response:
101 """Return a 404 JSON response when no favicon data exists.
103 Builds a standardized error body using the boilerplate response helper
104 and returns an HTTP 404 response with appropriate JSON headers.
107 title (str): Title to include in the response context.
108 token (Optional[str]): Optional token related to the request.
111 Response: A FastAPI response with HTTP 404 and JSON body.
113 self.
disp.log_error(
"There is no data available.")
116 message=
"No icon available",
121 return HCI.not_found(
123 content_type=HttpDataTypes.JSON,
128 """Replace numeric id fields in a favicon data dict with full records.
130 Mutates the provided `data` mapping by replacing `type`, `gender` and
131 `season` fields (when present) with the corresponding full records from
135 data (Dict[str, Any]): The raw favicon data containing id fields.
138 Dict[str, Any]: The mutated mapping with populated fields.
146 """Normalize a favicon type identifier to a numeric id.
148 Accepts either an integer id or a string name/id and returns the
149 corresponding integer id when found, otherwise `None`.
152 ftype (Optional[Union[int, str]]): The id or name to resolve.
155 Optional[int]: The resolved integer id or `None` if not found.
157 if not isinstance(ftype, (int, str)):
160 ftype_str = str(ftype)
163 if str(fid) == ftype_str:
165 if item.get(
"name") == str(ftype):
170 """Normalize a favicon gender identifier to a numeric id.
172 Accepts either an integer id or a string name/id and returns the
173 corresponding integer id when found, otherwise `None`.
176 gender (Optional[Union[int, str]]): The id or name to resolve.
179 Optional[int]: The resolved integer id or `None` if not found.
181 if not isinstance(gender, (int, str)):
184 gender_str = str(gender)
187 if str(gid) == gender_str:
189 if item.get(
"gender") == str(gender):
194 """Normalize a favicon season identifier to a numeric id.
196 Accepts either an integer id or a string name/id and returns the
197 corresponding integer id when found, otherwise `None`.
200 ftype (Optional[Union[int, str]]): The id or name to resolve.
203 Optional[int]: The resolved integer id or `None` if not found.
205 if not isinstance(ftype, (int, str)):
208 ftype_str = str(ftype)
211 if str(fid) == ftype_str:
213 if item.get(
"season") == str(ftype):
217 def _upload_bytes(self, image_data: bytes, icon_id: int, *, title: str =
"_upload_bytes") -> str:
218 """Upload image bytes to the configured bucket.
220 This is a placeholder for the actual implementation of the image
227 "There is no ImageReducer instance available in the runtime manager", title
229 raise FAV_ERR.FaviconNoImageReducerError(
230 "ImageReducer instance not found in RuntimeManager"
233 image_data).name.lower()
234 img_path = FAV_HELPERS.generate_image_path(
235 f
"{icon_id}.{file_format}", str(icon_id)
237 bucket_status = self.
bucket.upload_stream(
238 bucket_name=FAV_CONST.FAVICON_BUCKET_NAME,
242 if isinstance(bucket_status, int)
and bucket_status != self.
success:
243 err_msg = f
"Failed to upload favicon image data to bucket '{FAV_CONST.FAVICON_BUCKET_NAME}' at path '{img_path}'"
244 self.
disp.log_error(err_msg)
245 raise FAV_ERR.FaviconImageUploadError(err_msg)
248 def _upload_file(self, image_data: UploadFile, icon_id: int, *, title: str =
"_upload_bytes") -> str:
249 """Upload an UploadFile to the configured bucket.
251 This is a placeholder for the actual implementation of the image
258 "There is no ImageReducer instance available in the runtime manager", title
260 raise FAV_ERR.FaviconNoImageReducerError(
261 "ImageReducer instance not found in RuntimeManager"
263 file_data = image_data.file.read()
264 file_name = image_data.filename
265 image_data.file.close()
267 file_data).name.lower()
268 img_path = FAV_HELPERS.generate_image_path(
269 f
"{file_name}.{file_format}", str(icon_id)
271 bucket_status = self.
bucket.upload_stream(
272 bucket_name=FAV_CONST.FAVICON_BUCKET_NAME,
276 if isinstance(bucket_status, int)
and bucket_status != self.
success:
277 err_msg = f
"Failed to upload favicon image data to bucket '{FAV_CONST.FAVICON_BUCKET_NAME}' at path '{img_path}'"
278 self.
disp.log_error(err_msg)
279 raise FAV_ERR.FaviconImageUploadError(err_msg)
283 """(This is a wrapper of the same function in the constants)
284 Convert an ImageReducer FileFormat to an HttpDataTypes value.
287 reducer_type (IR_CONST.FileFormat): The image reducer file format.
290 HttpDataTypes: The corresponding HTTP data type.
292 return FAV_HELPERS.reducer_type_to_data_type(reducer_type)
295 """List all favicon genders from the database.
297 Returns a list of dictionary records representing available genders.
300 List[Dict[str, Any]]: The gender records.
302 table: str = FAV_CONST.FAVICON_TABLE_GENDER
303 title =
"list_favicon_gender:list_from_table"
304 return FAV_HELPERS.list_from_table(self.
sql, table, title=title, disp=self.
disp)
307 """List all favicon seasons from the database.
309 Returns a list of dictionary records representing available seasons.
312 List[Dict[str, Any]]: The season records.
314 table: str = FAV_CONST.FAVICON_TABLE_SEASON
315 title =
"list_favicon_season:list_from_table"
316 return FAV_HELPERS.list_from_table(self.
sql, table, title=title, disp=self.
disp)
319 """List all favicon types from the database.
321 Returns a list of dictionary records representing available types.
324 List[Dict[str, Any]]: The type records.
326 table: str = FAV_CONST.FAVICON_TABLE_TYPE
327 title =
"list_favicon_type:list_from_table"
328 return FAV_HELPERS.list_from_table(self.
sql, table, title=title, disp=self.
disp)
331 """Return list of favicons with resolved type/gender/season fields.
333 Fetches raw favicon records from the main table and replaces numeric
334 `type`, `gender` and `season` identifiers with full records resolved
335 via the helper list/get functions.
338 List[Dict[str, Any]]: The favicon records with populated fields.
340 table: str = FAV_CONST.FAVICON_TABLE_MAIN
341 title =
"list_favicons:list_from_table"
342 favicon = FAV_HELPERS.list_from_table(
343 self.
sql, table, title=title, disp=self.
disp)
344 if len(favicon) == 0:
345 self.
disp.log_debug(
"No favicons available.")
351 icon[
"type"] = FAV_HELPERS.extract_line_from_id(
352 data_list=f_type, entry_id=icon.get(
"type", -1), disp=self.
disp
354 icon[
"gender"] = FAV_HELPERS.extract_line_from_id(
355 data_list=f_gender, entry_id=icon.get(
"gender", -1), disp=self.
disp
357 icon[
"season"] = FAV_HELPERS.extract_line_from_id(
358 data_list=f_season, entry_id=icon.get(
"season", -1), disp=self.
disp
363 """Retrieve a single favicon gender record by id.
366 item_id (Union[int, str]): The id (or identifier) of the gender.
369 Dict[str, Any]: The gender record or an empty dict if not found.
371 table: str = FAV_CONST.FAVICON_TABLE_GENDER
372 title =
"get_favicon_gender:get_from_table"
373 return FAV_HELPERS.get_from_table(self.
sql, table, item_id, title=title, disp=self.
disp)
376 """Retrieve a single favicon season record by id.
379 item_id (Union[int, str]): The id (or identifier) of the season.
382 Dict[str, Any]: The season record or an empty dict if not found.
384 table: str = FAV_CONST.FAVICON_TABLE_SEASON
385 title =
"get_favicon_season:get_from_table"
386 return FAV_HELPERS.get_from_table(self.
sql, table, item_id, title=title, disp=self.
disp)
389 """Retrieve a single favicon type record by id.
392 item_id (Union[int, str]): The id (or identifier) of the type.
395 Dict[str, Any]: The type record or an empty dict if not found.
397 table: str = FAV_CONST.FAVICON_TABLE_TYPE
398 title =
"get_favicon_type:get_from_table"
399 return FAV_HELPERS.get_from_table(self.
sql, table, item_id, title=title, disp=self.
disp)
401 def get_favicon(self, favicon_id, *, fetch_image: bool =
True, title: str =
"list_user_favicon", token: Optional[str] =
None) -> Union[FAV_CONST.FaviconData, Response]:
402 """Retrieve a favicon record and optionally its binary image.
405 favicon_id: The id of the favicon to retrieve.
406 fetch_image (bool): If True, attempt to download image bytes from the
407 configured bucket. If False, return metadata only. Defaults to True.
408 title (str): Title used in logging and error responses.
409 token (Optional[str]): Optional token used in error responses.
412 Union[FAV_CONST.FaviconData, Response]: A `FaviconData` object with
413 metadata and optionally `img`/`img_type` populated, or a FastAPI
414 `Response` when an error response should be returned.
416 table: str = FAV_CONST.FAVICON_TABLE_MAIN
417 final_resp: FAV_CONST.FaviconData = FAV_CONST.FaviconData()
419 f
"Gathering the list of uploaded user icons from table '{table}'"
421 sql_resp = self.
sql.get_data_from_table(
424 where=f
"id={favicon_id}"
426 if isinstance(sql_resp, int):
428 f
"Failed to gather data for table '{table}'"
431 if len(sql_resp) == 0:
433 final_resp.data = CONST.clean_dict(
437 (FAV_CONST.FAVICON_IMAGE_PATH_KEY,
""),
440 if sql_resp[0][FAV_CONST.FAVICON_IMAGE_PATH_KEY]
is None or sql_resp[0][FAV_CONST.FAVICON_IMAGE_PATH_KEY] ==
"":
442 f
"There is no image path for icon id='{favicon_id}'"
447 "Image fetch not requested; returning data without image."
454 "There is no ImageReducer instance available in the runtime manager"
457 img_path = sql_resp[0][FAV_CONST.FAVICON_IMAGE_PATH_KEY]
458 bucket_resp = self.
bucket.download_stream(
459 bucket_name=FAV_CONST.FAVICON_BUCKET_NAME,
462 if isinstance(bucket_resp, int):
464 f
"Failed to download image data from bucket '{FAV_CONST.FAVICON_BUCKET_NAME}' and path '{img_path}'"
467 final_resp.img = bucket_resp
472 f
"Data gathered for table '{table}':\n{final_resp}"
476 def register_gender(self, gender: str, *, title: str =
"register_favicon_gender") -> int:
477 """Register a new favicon gender into the genders table.
480 gender (str): The gender label to insert.
481 title (str): Logging/response title. Defaults to "register_favicon_gender".
484 int: `self.success` on success or an error code from the SQL layer.
486 table: str = FAV_CONST.FAVICON_TABLE_GENDER
488 f
"Registering new favicon gender '{gender}' into table '{table}'", title
490 column_clean = [
"gender"]
491 status = self.
sql.insert_data_into_table(
498 f
"Failed to register new favicon gender '{gender}' into table '{table}'", title
502 f
"Registered new favicon gender '{gender}' into table '{table}' successfully", title
506 def register_season(self, season: str, parent_season: Optional[int] =
None, *, title: str =
"register_favicon_season") -> int:
507 """Register a new favicon season, optionally with a parent season.
510 season (str): The season label to insert.
511 parent_season (Optional[int]): Optional id of a parent season.
512 title (str): Logging/response title. Defaults to "register_favicon_season".
515 int: `self.success` on success or an error code from the SQL layer.
517 table: str = FAV_CONST.FAVICON_TABLE_SEASON
519 f
"Registering new favicon season '{season}' with parent '{parent_season}' into table '{table}'", title
521 column_clean = [
"season"]
522 data_clean: List[Union[str, int, float,
None]] = [season]
523 if isinstance(parent_season, int):
525 if not data
or data.get(
"id") != parent_season
or "season" not in data:
527 f
"Parent season id '{parent_season}' does not exist in table '{table}'", title
530 column_clean.append(
"parent_id")
531 data_clean.append(parent_season)
532 status = self.
sql.insert_data_into_table(
539 f
"Failed to register new favicon season '{season}' into table '{table}'", title
543 f
"Registered new favicon season '{season}' into table '{table}' successfully", title
547 def register_type(self, ftype: str, parent_type: Optional[int] =
None, *, title: str =
"register_favicon_type") -> int:
548 """Register a new favicon type, optionally linked to a parent type.
551 ftype (str): The type name to insert.
552 parent_type (Optional[int]): Optional id of a parent type.
553 title (str): Logging/response title. Defaults to "register_favicon_type".
556 int: `self.success` on success or an error code from the SQL layer.
558 table: str = FAV_CONST.FAVICON_TABLE_TYPE
560 f
"Registering new favicon type '{ftype}' with parent '{parent_type}' into table '{table}'", title
562 column_clean = [
"name"]
563 data_clean: List[Union[str, int, float,
None]] = [ftype]
564 if isinstance(parent_type, int):
566 if not data
or data.get(
"id") != parent_type
or "name" not in data:
568 f
"Parent season id '{parent_type}' does not exist in table '{table}'", title
571 column_clean.append(
"parent_id")
572 data_clean.append(parent_type)
573 status = self.
sql.insert_data_into_table(
580 f
"Failed to register new favicon type '{ftype}' into table '{table}'", title
584 f
"Registered new favicon type '{ftype}' into table '{table}' successfully", title
591 price: int = FAV_CONST.FAVICON_DEFAULT_PRICE,
592 ftype: Optional[Union[int, str]] =
None,
593 gender: Optional[Union[int, str]] =
None,
594 season: Optional[Union[int, str]] =
None,
595 default_colour: Optional[str] =
None,
596 image_data: Optional[Union[bytes, UploadFile]] =
None,
597 source: Optional[str] =
None,
598 *, title: str =
"register_icon"
600 """Register a new favicon entry and optionally upload its image.
603 name (str): The display name for the favicon.
604 price (int): The price associated with the favicon.
605 ftype (Optional[Union[int, str]]): Type id or name to link.
606 gender (Optional[Union[int, str]]): Gender id or name to link.
607 season (Optional[Union[int, str]]): Season id or name to link.
608 default_colour (Optional[str]): Hex colour string for default colour.
609 image_data (Optional[Union[bytes, UploadFile]]): Optional image bytes to upload to the bucket.
610 source (Optional[str]): Optional source metadata for the icon.
611 title (str): Logging/response title.
614 int: The id of the newly created favicon.
617 FAV_ERR.FaviconDatabaseError: If inserting the favicon metadata or
618 retrieving the new id from the database fails.
619 FAV_ERR.FaviconNoImageReducerError: If an ImageReducer instance is not
620 available in the runtime manager when `image_data` is provided.
621 FAV_ERR.FaviconImageUploadError: If uploading image bytes to the
622 configured bucket fails.
623 FAV_ERR.FaviconImagePathUpdateError: If updating the database with
624 the stored image path fails after upload.
626 The function inserts the favicon metadata into the main table, resolves
627 type/gender/season identifiers, and if `image_data` is provided it will
628 upload the image to the configured bucket and update the database with
629 the stored image path. Various filesystem/bucket/database errors are
630 raised as specialized `FAV_ERR` exceptions or returned as error codes.
632 table: str = FAV_CONST.FAVICON_TABLE_MAIN
634 f
"Registering new favicon '{name}' into table '{table}'", title
636 column_clean = [
"name",
"price"]
637 data_clean: List[Union[str, int, float,
None]] = [name, price]
639 if isinstance(ftype_id, int):
640 column_clean.append(
"type")
641 data_clean.append(ftype_id)
643 if isinstance(gender_id, int):
644 column_clean.append(
"gender")
645 data_clean.append(gender_id)
647 if isinstance(season_id, int):
648 column_clean.append(
"season")
649 data_clean.append(season_id)
650 if isinstance(default_colour, str)
and FAV_HELPERS.is_hex_colour_valid(default_colour):
651 padded_colour = FAV_HELPERS.pad_hex_colour(
652 default_colour, with_alpha=
True
654 column_clean.append(
"default_colour")
655 data_clean.append(padded_colour)
656 if isinstance(source, str):
657 column_clean.append(
"source")
658 data_clean.append(source)
659 status = self.
sql.insert_data_into_table(
666 f
"Failed to register new favicon '{name}' into table '{table}'", title
668 raise FAV_ERR.FaviconDatabaseError(
669 "Failed to insert new favicon into database"
672 f
"Registered new favicon '{name}' into table '{table}' successfully", title
677 f
"Failed to retrieve the id of the newly registered favicon '{name}'", title
679 raise FAV_ERR.FaviconDatabaseError(
680 "Could not retrieve newly inserted favicon id"
682 self.
disp.log_debug(f
"New favicon id is '{icon_id}'", title)
684 f
"type(Image data) = '{type(image_data)}', image_data={image_data}", title
686 if isinstance(image_data, bytes):
687 self.
disp.log_debug(
"Uploading image from bytes...", title)
691 title=f
"{title}:_upload_bytes"
693 elif isinstance(image_data, (UploadFile, StarletteUploadFile)):
694 self.
disp.log_debug(
"Uploading image from UploadFile...", title)
698 title=f
"{title}:_upload_file"
702 f
"Image data is of unsupported type '{type(image_data)}'; skipping upload.", title
705 "No image data provided; skipping upload.", title
708 update_status = self.
sql.update_data_in_table(
712 where=f
"id={icon_id}"
714 if update_status != self.
success:
716 f
"Failed to update favicon image path in table '{table}' for icon id '{icon_id}'", title
718 raise FAV_ERR.FaviconImagePathUpdateError(
719 "Failed to update image path in database"
722 f
"Uploaded favicon image data to bucket '{FAV_CONST.FAVICON_BUCKET_NAME}' at path '{img_path}' successfully", title
729 image_data: Optional[Union[bytes, UploadFile]] =
None,
730 *, title: str =
"register_icon"
732 """Register a new favicon entry and optionally upload its image.
735 name (str): The display name for the favicon.
736 price (int): The price associated with the favicon.
737 ftype (Optional[Union[int, str]]): Type id or name to link.
738 gender (Optional[Union[int, str]]): Gender id or name to link.
739 season (Optional[Union[int, str]]): Season id or name to link.
740 default_colour (Optional[str]): Hex colour string for default colour.
741 image_data (Optional[Union[bytes, UploadFile]]): Optional image bytes to upload to the bucket.
742 source (Optional[str]): Optional source metadata for the icon.
743 title (str): Logging/response title.
746 int: The id of the newly created favicon.
749 FAV_ERR.FaviconDatabaseError: If inserting the favicon metadata or
750 retrieving the new id from the database fails.
751 FAV_ERR.FaviconNoImageReducerError: If an ImageReducer instance is not
752 available in the runtime manager when `image_data` is provided.
753 FAV_ERR.FaviconImageUploadError: If uploading image bytes to the
754 configured bucket fails.
755 FAV_ERR.FaviconImagePathUpdateError: If updating the database with
756 the stored image path fails after upload.
758 The function inserts the favicon metadata into the main table, resolves
759 type/gender/season identifiers, and if `image_data` is provided it will
760 upload the image to the configured bucket and update the database with
761 the stored image path. Various filesystem/bucket/database errors are
762 raised as specialized `FAV_ERR` exceptions or returned as error codes.
764 table: str = FAV_CONST.FAVICON_TABLE_MAIN
766 f
"Registering new favicon image '{icon_id}' into table '{table}'", title
769 f
"type(Image data) = '{type(image_data)}', image_data={image_data}", title
771 if isinstance(image_data, bytes):
772 self.
disp.log_debug(
"Uploading image from bytes...", title)
776 title=f
"{title}:_upload_bytes"
778 elif isinstance(image_data, (UploadFile, StarletteUploadFile)):
779 self.
disp.log_debug(
"Uploading image from UploadFile...", title)
783 title=f
"{title}:_upload_file"
787 f
"Image data is of unsupported type '{type(image_data)}'; skipping upload.", title
790 "No image data provided; skipping upload.", title
793 update_status = self.
sql.update_data_in_table(
797 where=f
"id={icon_id}"
799 if update_status != self.
success:
801 f
"Failed to update favicon image path in table '{table}' for icon id '{icon_id}'", title
803 raise FAV_ERR.FaviconImagePathUpdateError(
804 "Failed to update image path in database"
807 f
"Uploaded favicon image data to bucket '{FAV_CONST.FAVICON_BUCKET_NAME}' at path '{img_path}' successfully", title
int register_season(self, str season, Optional[int] parent_season=None, *, str title="register_favicon_season")
Dict[str, Any] _populate_ids(self, Dict[str, Any] data)
Response _no_user_id(self, str title, Optional[str] token=None)
int register_type(self, str ftype, Optional[int] parent_type=None, *, str title="register_favicon_type")
Optional[int] _process_favicon_gender_id(self, Optional[Union[int, str]] gender=None)
List[Dict[str, Any]] list_favicon_gender(self)
str _upload_file(self, UploadFile image_data, int icon_id, *, str title="_upload_bytes")
Union[FAV_CONST.FaviconData, Response] get_favicon(self, favicon_id, *, bool fetch_image=True, str title="list_user_favicon", Optional[str] token=None)
None __init__(self, int success=0, int error=84, bool debug=False)
int register_gender(self, str gender, *, str title="register_favicon_gender")
Response _no_data(self, str title, Optional[str] token=None)
List[Dict[str, Any]] list_favicons(self)
str _upload_bytes(self, bytes image_data, int icon_id, *, str title="_upload_bytes")
Optional[int] _process_favicon_season_id(self, Optional[Union[int, str]] ftype=None)
HttpDataTypes reducer_type_to_data_type(self, FileFormat reducer_type)
Dict[str, Any] get_favicon_gender(self, Union[int, str] item_id=1)
Dict[str, Any] get_favicon_season(self, Union[int, str] item_id=1)
Optional[int] _process_favicon_type_id(self, Optional[Union[int, str]] ftype=None)
RuntimeManager runtime_manager
RuntimeManager server_header
List[Dict[str, Any]] list_favicon_type(self)
int register_icon_image(self, int icon_id, Optional[Union[bytes, UploadFile]] image_data=None, *, str title="register_icon")
int register_icon(self, str name, int price=FAV_CONST.FAVICON_DEFAULT_PRICE, Optional[Union[int, str]] ftype=None, Optional[Union[int, str]] gender=None, Optional[Union[int, str]] season=None, Optional[str] default_colour=None, Optional[Union[bytes, UploadFile]] image_data=None, Optional[str] source=None, *, str title="register_icon")
RuntimeManager boilerplate_response
Dict[str, Any] get_favicon_type(self, Union[int, str] item_id=1)
List[Dict[str, Any]] list_favicon_season(self)