Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
favicon_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: favicon_helpers.py
14# CREATION DATE: 12-01-2026
15# LAST Modified: 22:17:14 12-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 containing the functions that will help the favicon classes in the different processes.
21# // AR
22# +==== END CatFeeder =================+
23"""
24
25from typing import List, Dict, Any, Union, Optional, TYPE_CHECKING
26from pathlib import Path
27from display_tty import Disp, initialise_logger
28from ..config.env_loader import EnvLoader
29from ..image_reducer import image_reducer_constants as IR_CONST
30from ..http_codes import HttpDataTypes
31from . import favicon_constants as FAV_CONST
32
33if TYPE_CHECKING:
34 from ..sql import SQL
35
36FAVICON_HELPER_DISP: Disp = initialise_logger(
37 class_name="FaviconHelperDisp", debug=EnvLoader().debug
38)
39
40
41def reducer_type_to_data_type(reducer_type: IR_CONST.FileFormat, *, disp: Disp = FAVICON_HELPER_DISP) -> HttpDataTypes:
42 """Convert an ImageReducer FileFormat to an HttpDataTypes value.
43
44 Args:
45 reducer_type (IR_CONST.FileFormat): The image reducer file format.
46
47 Returns:
48 HttpDataTypes: The corresponding HTTP data type.
49 """
50 disp.log_debug(
51 f"Converting reducer type '{reducer_type}' to HTTP data type."
52 )
53 if reducer_type == IR_CONST.FileFormat.PNG:
54 disp.log_debug("Matched PNG format.")
55 return HttpDataTypes.PNG
56 if reducer_type == IR_CONST.FileFormat.JPEG:
57 disp.log_debug("Matched JPEG format.")
58 return HttpDataTypes.JPEG
59 if reducer_type == IR_CONST.FileFormat.WEBP:
60 disp.log_debug("Matched WEBP format.")
61 return HttpDataTypes.WEBP
62 if reducer_type == IR_CONST.FileFormat.SVG:
63 disp.log_debug("Matched SVG format.")
64 return HttpDataTypes.SVG
65 disp.log_debug("No matching format found. Defaulting to OCTET_STREAM.")
66 return HttpDataTypes.OCTET_STREAM
67
68
69def generate_image_path(filename: str, file_id: str) -> str:
70 """Generate the full image path for a favicon in the bucket.
71
72 Args:
73 filename (str): The filename of the image.
74 file_id (str): The unique identifier for the favicon.
75
76 Returns:
77 str: The full path to the image in the bucket.
78 """
79 file_format = Path(filename).name.rsplit(
80 ".", maxsplit=1
81 )[-1] # Sanitize the filename
82 return str(Path(FAV_CONST.FAVICON_BUCKET_FOLDER_USER) / f"{file_id}.{file_format}")
83
84
85def list_from_table(sql: "SQL", table: str, *, title: str = "_list_from_table", disp: Disp = FAVICON_HELPER_DISP) -> List[Dict[str, Any]]:
86 """Retrieve all rows from `table` using the provided SQL helper.
87
88 This wraps the low-level SQL `get_data_from_table` call and returns a
89 list of dictionaries. On SQL failure the function logs the error and
90 returns an empty list.
91
92 Args:
93 sql (SQL): SQL helper instance with `get_data_from_table`.
94 table (str): Table name to query.
95 title (str): Optional logging title.
96 disp (Disp): Optional display logger.
97
98 Returns:
99 List[Dict[str, Any]]: The list of rows (possibly empty on error).
100 """
101 disp.log_debug(
102 f"Gathering the list of entries from table '{table}'", title
103 )
104 resp = sql.get_data_from_table(
105 table=table,
106 column="*"
107 )
108 if isinstance(resp, int):
109 disp.log_error(
110 f"Failed to gather data for table '{table}'", title
111 )
112 disp.log_error(
113 "Returning []", title
114 )
115 return []
116 disp.log_debug(
117 f"Data gathered for table '{table}':\n{resp}", title
118 )
119 return resp
120
121
122def extract_line_from_id(data_list: List[Dict[str, Any]], entry_id: Optional[Union[int, str]], *, disp: Disp = FAVICON_HELPER_DISP) -> Dict[str, Any]:
123 """Extract a line from a list of dictionaries based on the 'id' key.
124
125 Args:
126 data_list (List[Dict[str, Any]]): The list of dictionaries to search.
127 entry_id (Union[int,str]): The id to search for.
128 disp (Disp, optional): The display logger to use.
129
130 Returns:
131 Dict[str, Any]: The dictionary with the matching id. If no matching
132 entry is found the function returns a dictionary containing only the
133 `'id'` key with the original `entry_id` value (e.g. `{'id': entry_id}`).
134 """
135 if not entry_id:
136 disp.log_debug(
137 "No entry_id provided, returning empty dictionary.", "extract_line_from_id"
138 )
139 return {'id': entry_id}
140 disp.log_debug(
141 f"Extracting entry with id '{entry_id}' from data list.", "extract_line_from_id"
142 )
143 entry_id_str: str = str(entry_id)
144 for entry in data_list:
145 if str(entry.get("id")) == entry_id_str:
146 disp.log_debug(
147 f"Entry found: {entry}", "extract_line_from_id"
148 )
149 return entry
150 disp.log_debug(
151 f"No entry found with id '{entry_id}'. Returning empty dictionary.", "extract_line_from_id"
152 )
153 return {'id': entry_id}
154
155
156def get_from_table(sql: "SQL", table: str, item_id: Union[int, str], *, title: str = "get_from_table", disp: Disp = FAVICON_HELPER_DISP) -> Dict[str, Any]:
157 """Retrieve a single row by id from `table`.
158
159 Args:
160 sql (SQL): SQL helper instance.
161 table (str): Table name to query.
162 item_id (Union[int, str]): The id value to look up.
163 title (str): Optional logging title.
164 disp (Disp): Optional display logger.
165
166 Returns:
167 Dict[str, Any]: The found row as a dictionary. If the SQL call fails
168 the function returns a dict with `{'id': id}`. If the query succeeds
169 but no rows are found it returns an empty dict.
170 """
171 disp.log_debug(
172 f"Gathering a single entry from table '{table}'", title
173 )
174 resp = sql.get_data_from_table(
175 table=table,
176 column="*",
177 where=f"id={item_id}",
178 beautify=True
179 )
180 if isinstance(resp, int):
181 disp.log_error(
182 f"Failed to gather data for table '{table}'", title
183 )
184 disp.log_error(
185 "Returning empty dictionary", title
186 )
187 return {'id': id}
188 if len(resp) == 0:
189 disp.log_debug(
190 f"No entries found in table '{table}'. Returning empty dictionary.", title
191 )
192 return {}
193 disp.log_debug(
194 f"Data gathered for table '{table}':\n{resp[0]}", title
195 )
196 return resp[0]
197
198
199def is_hex_colour_valid(colour: str) -> bool:
200 """Check if a string is a valid hex colour code.
201
202 Args:
203 colour (str): The colour string to validate.
204 Returns:
205 bool: True if valid hex colour, False otherwise.
206 """
207 if isinstance(colour, str):
208 return bool(FAV_CONST.FAVICON_HEX_COLOUR_RE.fullmatch(colour))
209 return False
210
211
212def pad_hex_colour(colour: str, with_alpha: bool = True) -> str:
213 """
214 Normalize a hex colour to full form.
215
216 - #RGB → #RRGGBB
217 - #RGBA → #RRGGBBAA
218 - #RRGGBB → #RRGGBB or #RRGGBBFF
219 - #RRGGBBAA → unchanged
220
221 Args:
222 colour (str): Hex colour
223 with_alpha (bool): If True, ensure alpha channel exists
224
225 Returns:
226 str: Normalized hex colour
227 """
228 if not is_hex_colour_valid(colour):
229 return colour
230
231 hex_part = colour[1:]
232
233 if len(hex_part) in (3, 4):
234 # Expand shorthand
235 hex_part = ''.join(c * 2 for c in hex_part)
236
237 if len(hex_part) == 6 and with_alpha:
238 hex_part += 'FF' # fully opaque
239
240 return '#' + hex_part.upper()
241
242
243def unpad_hex_colour(colour: str) -> str:
244 """
245 Compress a hex colour to shorthand when safe.
246
247 - #RRGGBB → #RGB
248 - #RRGGBBAA → #RGBA
249 - Otherwise unchanged
250 """
251 if not is_hex_colour_valid(colour):
252 return colour
253
254 hex_part = colour[1:]
255
256 if len(hex_part) not in (6, 8):
257 return colour
258
259 step = 2
260 compressed = []
261
262 for i in range(0, len(hex_part), step):
263 pair = hex_part[i:i + 2]
264 if pair[0] != pair[1]:
265 return colour
266 compressed.append(pair[0])
267
268 return '#' + ''.join(compressed).upper()
Dict[str, Any] extract_line_from_id(List[Dict[str, Any]] data_list, Optional[Union[int, str]] entry_id, *, Disp disp=FAVICON_HELPER_DISP)
List[Dict[str, Any]] list_from_table("SQL" sql, str table, *, str title="_list_from_table", Disp disp=FAVICON_HELPER_DISP)
HttpDataTypes reducer_type_to_data_type(IR_CONST.FileFormat reducer_type, *, Disp disp=FAVICON_HELPER_DISP)
str pad_hex_colour(str colour, bool with_alpha=True)
Dict[str, Any] get_from_table("SQL" sql, str table, Union[int, str] item_id, *, str title="get_from_table", Disp disp=FAVICON_HELPER_DISP)
str generate_image_path(str filename, str file_id)