ThalamOS
a powerful Flask web application designed to enhance your storage management.
Loading...
Searching...
No Matches
app.py
Go to the documentation of this file.
1"""
2ThalamOS Application
3
4This module sets up a Flask web application for managing storage items, controlling WLED devices,
5interacting with a WiFi scale, and querying an LLM service. It provides various routes for rendering templates,
6handling item creation, deletion, and search, as well as retrieving environment configurations,
7scale weight, and logging messages.
8
9Routes:
10 - /: Renders the search page.
11 - /toggleLight: Toggles the power state of the WLED device and renders the search template.
12 - /createItem: Renders the template for creating a new item.
13 - /sendCreation: Handles the creation of a new item by processing the incoming JSON request data.
14 - /item/<item_id>: Handles the request to display an item.
15 - /item/<item_id>/update: Updates an item using the Storage_connector and returns a status message.
16 - /item/<item_id>/delete: Deletes an item using the Storage_connector and renders the search.html template.
17 - /search/<term>: Searches for a term in the storage and returns the results in JSON format.
18 - /config/env: Retrieves the environment configuration.
19 - /config/ollama/models: Fetches the list of available Ollama models.
20 - /wifiscale/weight: Retrieves the weight of the scale.
21 - /llm/ask: Asks a question to the LLM service and returns the response.
22 - /log: Logs a message with a specified log level.
23
24Error Handling:
25 - handle_exception: Handles exceptions by passing through HTTP errors.
26
27Setup:
28 - Initializes the Flask app and sets up CORS.
29 - Loads environment variables from a .env file.
30 - Sets up the Storage_connector within the app context.
31
32Usage:
33 Run the application using the command `python app.py`.
34"""
35
36from typing import Annotated
37import json
38import os
39
40from flask import ( # pylint: disable=import-error
41 Flask,
42 request,
43 render_template,
44 jsonify,
45 Response,
46)
47from flask_cors import CORS # pylint: disable=import-error
48from dotenv import load_dotenv # pylint: disable=import-error
49
50from logger_config import logger
51import Storage_connector
52import config_manager
53import weigh_fi_manager as wifiscale
54import wled_requests
55import ollama_manager as ollama
56
57ENV_PATH = os.path.join(os.path.dirname(__file__), "data/.env")
58load_dotenv(dotenv_path=ENV_PATH)
59IS_SCALE_ENABLED = os.getenv("IS_SCALE_ENABLED").lower() == "true"
60app = Flask(__name__)
61CORS(app)
62
63
64@app.route("/")
65def index() -> Annotated[str, "search page as a rendered template"]:
66 """
67 Renders the search page.
68 Returns:
69 Response: The rendered HTML template for the search page.
70 """
71 return render_template("search.html")
72
73
74@app.route("/toggleLight")
75def toggle_light() -> Annotated[str, "search page as a rendered template"]:
76 """
77 Toggles the power state of the WLED device and renders the search template.
78 This function changes the power state of the WLED device to the opposite of its current state
79 by calling the `changePowerState` method of the `wled_requests` object.
80 After toggling the power state,
81 it returns the rendered "search.html" template.
82 Returns:
83 str: The rendered "search.html" template.
84 """
86 return render_template("search.html")
87
88
89@app.route("/createItem")
90def create_item() -> Annotated[str, "item creation page as a rendered template"]:
91 """
92 Renders the template for creating a new item.
93 Returns:
94 Response: The rendered HTML template for creating a new item.
95 """
96 return render_template("createItem.html")
97
98
99@app.route("/sendCreation", methods=["POST"])
100def send_creation() -> Annotated[tuple, {"status": str, "status_code": int}]:
101 """
102 Handle the creation of a new item by processing the incoming JSON request data.
103 The function expects a JSON payload with the following structure:
104 {
105 "info": <json>,
106 "type": <str>,
107 "name": <str>,
108 "position": <str>
109 }
110 It extracts the necessary information from the JSON payload and attempts to create a new item
111 using the Storage_connector.CreateItem method.
112 If an exception occurs during the creation process,
113 it prints the exception.
114 Returns:
115 tuple: A dictionary with a status message and an HTTP status code.
116 """
117
118 data = request.get_json()
119 logger.info(f"Received creation request with data: {data}")
120 info = json.dumps(data["info"])
121 obj_type = data["type"]
122 name = data["name"]
123 pos = data["position"]
124 try:
126 pos=pos, obj_type=obj_type, name=name, json_data=info
127 )
128 except Exception as e:
129 logger.error(
130 f"Failed to create item with name: {name}, type: {obj_type}, position: {pos}. Error: {e}"
131 )
132 return {"status": "error"}, 500
133 return {"status": "created"}, 201
134
135
136@app.route("/item/<item_id>")
137def item(item_id) -> Annotated[str, "item page as a rendered template"]:
138 """
139 Handles the request to display an item.
140 This function performs the following steps:
141 1. Changes the power state of the WLED device to on.
142 2. Fetches the item details from the storage using the provided item identifier.
143 3. Sets the color position on the WLED device based on the fetched item details.
144 4. If the fetched item contains additional information,
145 it parses the data and renders the 'item.jinja2' template
146 with the item details and information.
147 6. If the fetched item does not contain additional information,
148 it renders the 'item.jinja2' template with only the item details.
149 Args:
150 item_id (int): The id of the item to display.
151 Returns:
152 The rendered HTML template for the item.
153 """
155 item_sql = Storage_connector.fetch_item(item_id)
156 wled_requests.color_pos(item_sql[1])
157 logger.info(f"Fetched item details for item_id {item_id}: {item_sql}")
158 if item_sql[4]:
159 json_info = json.loads(item_sql[4])
160 return render_template("item.jinja2", item=item_sql, json=json_info, id=item_id)
161
162 return render_template("item.jinja2", item=item_sql, id=item_id)
163
164
165@app.route("/item/<item_id>/update", methods=["POST"])
166def update_item(item_id) -> Annotated[tuple, {"status": str, "status_code": int}]:
167 """
168 Updates an item using the Storage_connector and returns a status message.
169 Args:
170 item_id: The id of the item to be updated.
171 Returns:
172 A dictionary with a status message and an HTTP status code.
173 """
174 data = request.get_json()
175 logger.info(f"Received update request for item_id {item_id} with data: {data}")
176 info = json.dumps(data.get("info", {}))
177 obj_type = data.get("type")
178 name = data.get("name")
179 pos = data.get("position")
180 try:
182 item_id=item_id, pos=pos, obj_type=obj_type, name=name, json_data=info
183 )
184 except Exception as e:
185 logger.error(f"Failed to update item with id: {item_id}. Error: {e}")
186 return {"status": "error"}, 500
187 return {"status": "updated"}, 200
188
189
190@app.route("/item/<item>/delete")
191def delete_item(item_id) -> Annotated[str, "search page as a rendered template"]:
192 """
193 Deletes an item using the Storage_connector and renders the search.html template.
194 Args:
195 item_id: The id of the item to be deleted.
196 Returns:
197 A rendered template for the search page.
198 """
199
201 return render_template("search.html")
202
203
204@app.route("/search/<term>", methods=["GET"])
205def search(term) -> Response:
206 """
207 Search for a term in the storage and return the results in JSON format.
208 Args:
209 term (str): The term to search for in the storage.
210 Returns:
211 Response: A Flask Response object containing the search results in JSON format.
212 """
213 data = Storage_connector.search(term)
214 return jsonify(data)
215
216
217@app.errorhandler(Exception)
218def handle_exception(e) -> Exception:
219 """
220 Handles exceptions by passing through HTTP errors.
221 Parameters:
222 e (Exception): The exception to handle.
223 Returns:
224 Exception: The same exception that was passed in.
225 """
226
227 # pass through HTTP errors
228 return e
229
230
231@app.route("/config/env")
232def get_env() -> Response:
233 """
234 Retrieve the environment configuration.
235 This function uses the config_manager to get the current environment
236 configuration and returns it as a JSON response.
237 Returns:
238 Response: A Flask JSON response containing the environment configuration.
239 """
240 return jsonify(config_manager.get_env())
241
242
243@app.route("/config/ollama/models")
244def get_ollama_models() -> tuple[Response, int] | Response:
245 """
246 Fetches the list of available Ollama models.
247 Returns:
248 Annotated[str, "json response"]: A JSON response containing the list of Ollama models.
249 """
250 values = ollama.get_ollama_models()
251 if not values:
252 return jsonify({"status": "Ollama service is not enabled"}), 412
253 return jsonify(values)
254
255
256@app.route("/wifiscale/weight")
257def get_weight() -> tuple[Response, int] | Response:
258 """
259 Retrieve the weight of the scale.
260 This function checks if the scale service is enabled by reading the IS_SCALE_ENABLED environment variable.
261 If the scale service is not enabled, it returns a JSON response with a status message and HTTP status code 412.
262 If the scale service is enabled, it uses the wifiscale module to get the weight of the scale and returns it as a JSON response.
263 Returns:
264 Response: A Flask JSON response containing the weight of the scale or a status message.
265 """
266 if IS_SCALE_ENABLED == "False":
267 return jsonify({"status": "scale service is not enabled"}), 412
268
269 weight = wifiscale.get_weight()
270 return jsonify({"weight": weight})
271
272
273@app.route("/llm/ask", methods=["POST"])
274def ask_llm_question() -> Response:
275 """
276 Ask a question to the Language Learning Model (LLM) service.
277 This function sends a GET request to the LLM service at the specified endpoint
278 and returns the response as a JSON object.
279 Returns:
280 Response: A Flask JSON response containing the response from the LLM service.
281 """
282 data = request.get_json()
283 question = data.get("question")
284 logger.info(f"Received question via llm endpoint: {question}")
285 response = ollama.ask_question(question)
286 return jsonify(response)
287
288
289@app.route("/log", methods=["POST"])
290def log_message() -> Annotated[tuple, {"status": str, "status_code": int}]:
291 """
292 Logs a message with a specified log level.
293
294 The log level and message content are extracted from the JSON payload of the request.
295 If the log level is not provided, it defaults to 'INFO'.
296 If the message content is not provided, it defaults to an empty string.
297
298 Returns:
299 tuple: A dictionary with a status message and an HTTP status code 201.
300
301 Request JSON structure:
302 {
303 "level": "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL",
304 "message": "Your log message here"
305 }
306 """
307
308 data: Annotated[str, "content of request"] = request.json
309 level: Annotated[str, "log level, default value is INFO"] = data.get(
310 "level", "INFO"
311 )
312 message: Annotated[str, "content of log, default value is empty"] = data.get(
313 "message", ""
314 )
315
316 match level:
317 case "DEBUG":
318 logger.debug(message)
319 case "INFO":
320 logger.info(message)
321 case "WARNING":
322 logger.warning(message)
323 case "ERROR":
324 logger.error(message)
325 case "CRITICAL":
326 logger.critical(message)
327
328 return {"status": "created"}, 201
329
330
331with app.app_context():
333
334
335if __name__ == "__main__":
336 app.run(host="0.0.0.0", debug=True)
Annotated[ list, "list of tuples containing the rows from the database that match the search criteria",] search(search_term)
None create_item(pos, obj_type, name, json_data)
None delete_item(item_id)
Annotated[tuple, "tuple containing the item's data if found, otherwise None"] fetch_item(item_id)
None update_item(item_id, pos, obj_type, name, json_data)
Response ask_llm_question()
Definition app.py:274
Exception handle_exception(e)
Definition app.py:218
Annotated[tuple, {"status":str, "status_code":int}] log_message()
Definition app.py:290
Annotated[tuple, {"status":str, "status_code":int}] send_creation()
Definition app.py:100
Annotated[tuple, {"status":str, "status_code":int}] update_item(item_id)
Definition app.py:166
Response search(term)
Definition app.py:205
tuple[Response, int]|Response get_ollama_models()
Definition app.py:244
Annotated[str, "item creation page as a rendered template"] create_item()
Definition app.py:90
tuple[Response, int]|Response get_weight()
Definition app.py:257
Annotated[str, "search page as a rendered template"] toggle_light()
Definition app.py:75
Annotated[str, "search page as a rendered template"] delete_item(item_id)
Definition app.py:191
Annotated[str, "item page as a rendered template"] item(item_id)
Definition app.py:137
Response get_env()
Definition app.py:232
Annotated[str, "search page as a rendered template"] index()
Definition app.py:65
Annotated[dict, "dictionary of environment variables"] get_env()
Annotated[bool, "True if power is on, false if power is off"] get_power_state()
None change_power_state(state)
None color_pos(int pos)