Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
cat_endpoints.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: cat_endpoints.py
14# CREATION DATE: 08-12-2025
15# LAST Modified: 21:33:16 05-02-2026
16# DESCRIPTION:
17# This is the project in charge of making the connected cat feeder project work.
18# /STOP
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: This is the file containing the endpoints used for the application of the cat feeder.
21# // AR
22# +==== END CatFeeder =================+
23"""
24from dataclasses import dataclass
25from typing import TYPE_CHECKING, Union, Optional
26from datetime import datetime, timezone, timedelta
27
28import requests
29from display_tty import Disp, initialise_logger
30
31from fastapi import Request, Response
32from ...utils import CONST
33from ...core import RuntimeManager, RI
34from .. import endpoint_helpers as EN_CONST
35from ...http_codes import HCI
36
37if TYPE_CHECKING:
38 from typing import List, Dict, Tuple
39 from ...sql import SQL
40 from ...server_header import ServerHeaders
41 from ...boilerplates import BoilerplateIncoming, BoilerplateResponses, BoilerplateNonHTTP
42
43
44@dataclass
46 """Dataclass to store user information.
47 """
48 user_id: int
49 username: str
50 email: str
51 is_admin: bool
52 token: str
53
54
56
57 disp: Disp = initialise_logger(__qualname__, False)
58
59 def __init__(self, error: int = 84, success: int = 0, debug: bool = False) -> None:
60 """_summary_
61 """
62 # ------------------------ The logging function ------------------------
63 self.disp.update_disp_debug(debug)
64 self.disp.log_debug("Initialising...")
65 # -------------------------- Inherited values --------------------------
66 self.error: int = error
67 self.success: int = success
68 self.debug: bool = debug
69 self.runtime_manager: RuntimeManager = RI
70 # ---------------------------- Table Names -----------------------------
71 self.tab_feeder: str = "Feeder"
72 self.tab_feeder_ip: str = "FeederIp"
73 self.tab_beacon: str = "Beacon"
74 self.tab_pet: str = "Pet"
75 self.tab_location_history: str = "Location_history"
76 self.cols_to_remove: Tuple[str, ...] = (
77 "id", "creation_date", "edit_date"
78 )
79 # -------------------------- forced timezone --------------------------
80 # self.forced_timezone: timezone = timezone.fromutc(timedelta(hours=0))
81 self.forced_timezone: timezone = timezone.utc
82 # -------------------------- Shared instances --------------------------
83 self.boilerplate_incoming_initialised: "BoilerplateIncoming" = self.runtime_manager.get(
84 "BoilerplateIncoming")
85 self.boilerplate_responses_initialised: "BoilerplateResponses" = self.runtime_manager.get(
86 "BoilerplateResponses")
87 self.boilerplate_non_http_initialised: "BoilerplateNonHTTP" = self.runtime_manager.get(
88 "BoilerplateNonHTTP")
89 self.database_link: "SQL" = self.runtime_manager.get("SQL")
90 self.server_headers_initialised: "ServerHeaders" = self.runtime_manager.get(
91 "ServerHeaders")
92 self.disp.log_debug("Initialised")
93
94 def _user_connected(self, request: Request, title: str = "_user_connected") -> Union[UserInfo, Response]:
95 """Check if the user is connected and return their information.
96
97 Args:
98 request (Request): The incoming request parameters.
99 Returns:
100 UserInfo: The user information if connected, None otherwise.
101 """
102 token = self.boilerplate_incoming_initialised.get_token_if_present(
103 request
104 )
105 if not token:
106 return self.boilerplate_responses_initialised.invalid_token(title)
107 if not self.boilerplate_non_http_initialised.is_token_correct(token):
108 return self.boilerplate_responses_initialised.invalid_token(title)
109 user_id = self.boilerplate_non_http_initialised.get_user_id_from_token(
110 title, token)
111 if isinstance(user_id, Response):
112 return user_id
113 if not user_id or user_id is None:
114 return self.boilerplate_responses_initialised.user_not_found(title, token)
115 usr_data = self.database_link.get_data_from_table(
116 CONST.TAB_ACCOUNTS,
117 "*",
118 f"id={user_id}",
119 beautify=True
120 )
121 if not isinstance(usr_data, list) or len(usr_data) == 0:
122 return self.boilerplate_responses_initialised.missing_resource(title, token)
123 return UserInfo(
124 user_id=usr_data[0]["id"],
125 username=usr_data[0]["username"],
126 email=usr_data[0]["email"],
127 is_admin=usr_data[0]["admin"],
128 token=token
129 )
130
131 # pick the most recent entry by edit_date (robust parsing)
132 def _parse_dt(self, val: Optional[str]) -> Optional[datetime]:
133 if val is None:
134 return None
135 if isinstance(val, datetime):
136 return val
137 try:
138 # try ISO first
139 dt = datetime.fromisoformat(val)
140 return dt
141 except (ValueError, TypeError):
142 # fallback common formats
143 for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
144 try:
145 return datetime.strptime(val, fmt)
146 except (ValueError, TypeError):
147 continue
148 return None
149
150 async def put_register_feeder(self, request: Request) -> Response:
151 """Register a new cat feeder in the database.
152
153 Args:
154 request (Request): The incoming request parameters.
155
156 Returns:
157 Response: The HTTP response to send back to the user.
158 """
159 title = "put_register_feeder"
160 data = self._user_connected(request, title)
161 if isinstance(data, Response):
162 return data
163 body = await self.boilerplate_incoming_initialised.get_body(request)
164 elems = [
165 "latitude", "longitude",
166 "city_locality", "country", "mac", "name"
167 ]
168 for elem in elems:
169 if elem not in body:
170 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, elem)
171 present = self.database_link.get_data_from_table(
172 self.tab_feeder,
173 ["id"],
174 f"owner={data.user_id} AND mac='{body['mac']}'",
175 beautify=True
176 )
177 if isinstance(present, list) and len(present) > 0:
178 return HCI.conflict(
179 self.boilerplate_responses_initialised.build_response_body(
180 title,
181 "Feeder with this MAC address already registered",
182 "exists",
183 data.token,
184 error=True
185 )
186 )
187 cols = self.database_link.get_table_column_names(self.tab_feeder)
188 if not isinstance(cols, list):
189 return self.boilerplate_responses_initialised.internal_server_error(
190 title, data.token
191 )
192 cols = CONST.clean_list(cols, self.cols_to_remove, self.disp)
193 sql_data = [
194 data.user_id,
195 body["latitude"],
196 body["longitude"],
197 body["city_locality"],
198 body["country"],
199 body["mac"],
200 body["name"]
201 ]
202 resp = self.database_link.insert_data_into_table(
203 self.tab_feeder,
204 sql_data,
205 cols
206 )
207 if resp == self.database_link.error:
208 return self.boilerplate_responses_initialised.internal_server_error(
209 title, data.token
210 )
211 bod = self.boilerplate_responses_initialised.build_response_body(
212 title, "Feeder registered successfully", "registered", data.token, error=False
213 )
214 return HCI.created(bod)
215
216 async def patch_feeder(self, request: Request) -> Response:
217 """Patch (partial update) of a cat feeder.
218
219 Args:
220 request (Request): The incoming request parameters.
221
222 Returns:
223 Response: The HTTP response to send back to the user.
224 """
225 title = "patch_feeder"
226 data = self._user_connected(request, title)
227 if isinstance(data, Response):
228 return data
229
230 body = await self.boilerplate_incoming_initialised.get_body(request)
231
232 # identifier: either id or mac must be provided
233 if "id" not in body and "mac" not in body:
234 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id or mac")
235
236 # allowed fields to update
237 allowed = {
238 "latitude", "longitude",
239 "city_locality", "country", "mac", "name"
240 }
241
242 cols = self.database_link.get_table_column_names(self.tab_feeder)
243 if not isinstance(cols, list):
244 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
245
246 # determine which columns from the table we can update based on request body
247 update_cols = [c for c in cols if c in allowed and c in body]
248 if not update_cols:
249 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "fields to update")
250
251 sql_data = [body[col] for col in update_cols]
252
253 # build where clause to ensure user updates only their feeder
254 if "id" in body:
255 try:
256 feeder_id = int(body["id"])
257 except (ValueError, TypeError):
258 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id")
259 where = f"id={feeder_id} AND owner={data.user_id}"
260 else:
261 where = f"owner={data.user_id} AND mac='{body['mac']}'"
262
263 resp = self.database_link.update_data_in_table(
264 self.tab_feeder,
265 sql_data,
266 update_cols,
267 where=where
268 )
269 if resp == self.database_link.error:
270 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
271
272 bod = self.boilerplate_responses_initialised.build_response_body(
273 title, "Feeder updated successfully", "updated", data.token, error=False
274 )
275 return HCI.success(bod)
276
277 async def get_feeder(self, request: Request) -> Response:
278 """Get the status of a cat feeder.
279
280 Args:
281 request (Request): The incoming request parameters.
282
283 Returns:
284 Response: The HTTP response to send back to the user.
285 """
286 title = "get_feeder"
287 data = self._user_connected(request, title)
288 if isinstance(data, Response):
289 return data
290
291 body = await self.boilerplate_incoming_initialised.get_body(request)
292
293 # require one of: id, name, or mac to identify the feeder
294 if "id" not in body and "name" not in body and "mac" not in body:
295 self.disp.log_debug(f"body={body}\n\n\n\n\n")
296 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id, name, or mac")
297
298 # build where clause
299 where_parts = [f"owner={data.user_id}"]
300 if "id" in body:
301 try:
302 feeder_id = int(body["id"])
303 where_parts.append(f"id={feeder_id}")
304 except (ValueError, TypeError):
305 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id")
306 elif "mac" in body:
307 where_parts.append(f"mac='{body['mac']}'")
308 else: # name
309 where_parts.append(f"name='{body['name']}'")
310
311 where = " AND ".join(where_parts)
312
313 # get feeder id
314 feeder_rows = self.database_link.get_data_from_table(
315 self.tab_feeder,
316 ["id"],
317 where,
318 beautify=True
319 )
320 if not isinstance(feeder_rows, list) or len(feeder_rows) == 0:
321 return HCI.not_found(
322 self.boilerplate_responses_initialised.build_response_body(
323 title,
324 "Feeder not found",
325 "not_found",
326 data.token,
327 error=True
328 )
329 )
330 data_raw: Dict = feeder_rows[0]
331 data_clean = EN_CONST.sanitize_response_data(
332 data_raw, disp=self.disp
333 )
334 return HCI.success(
335 self.boilerplate_responses_initialised.build_response_body(
336 title,
337 "Feeder found",
338 data_clean,
339 data.token,
340 error=False
341 )
342 )
343
344 async def get_feeder_status(self, request: Request) -> Response:
345 """Get the status of a cat feeder.
346
347 Args:
348 request (Request): The incoming request parameters.
349
350 Returns:
351 Response: The HTTP response to send back to the user.
352 """
353 title = "get_feeder_status"
354 data = self._user_connected(request, title)
355 if isinstance(data, Response):
356 return data
357
358 body = await self.boilerplate_incoming_initialised.get_body(request)
359
360 # require one of: id, name, or mac to identify the feeder
361 if "id" not in body and "name" not in body and "mac" not in body:
362 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id, name, or mac")
363
364 # build where clause
365 where_parts = [f"owner={data.user_id}"]
366 if "id" in body:
367 try:
368 feeder_id = int(body["id"])
369 where_parts.append(f"id={feeder_id}")
370 except (ValueError, TypeError):
371 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id")
372 elif "mac" in body:
373 where_parts.append(f"mac='{body['mac']}'")
374 else: # name
375 where_parts.append(f"name='{body['name']}'")
376
377 where = " AND ".join(where_parts)
378
379 # get feeder id
380 feeder_rows = self.database_link.get_data_from_table(
381 self.tab_feeder,
382 ["id"],
383 where,
384 beautify=True
385 )
386 if not isinstance(feeder_rows, list) or len(feeder_rows) == 0:
387 return HCI.not_found(
388 self.boilerplate_responses_initialised.build_response_body(
389 title,
390 "Feeder not found",
391 "not_found",
392 data.token,
393 error=True
394 )
395 )
396 feeder_id = feeder_rows[0]["id"]
397
398 # get IP history for this feeder
399 ip_rows = self.database_link.get_data_from_table(
400 self.tab_feeder_ip,
401 "*",
402 f"parent_id={feeder_id}",
403 beautify=True
404 )
405 if not isinstance(ip_rows, list) or len(ip_rows) == 0:
406 return HCI.not_found(
407 self.boilerplate_responses_initialised.build_response_body(
408 title,
409 "No IP record for this feeder",
410 "not_found",
411 data.token,
412 error=True
413 )
414 )
415
416 self.disp.log_debug(f"IP rows: {ip_rows}")
417 latest = None
418 latest_dt = None
419 for r in ip_rows:
420 self.disp.log_debug(f"Evaluating row: {r}")
421 dt = self._parse_dt(r.get("edit_date"))
422 self.disp.log_debug(
423 f"Parsed datetime: {dt} from edit_date: {r.get('edit_date')}"
424 )
425 if dt is None:
426 continue
427 if latest_dt is None or dt > latest_dt:
428 latest_dt = dt
429 latest = r
430
431 if latest is None or latest_dt is None:
432 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
433
434 # if last update older than 1 hour -> consider feeder offline
435 if datetime.now() - latest_dt > timedelta(hours=1):
436 bod = self.boilerplate_responses_initialised.build_response_body(
437 title,
438 "Feeder last seen more than 1 hour ago",
439 "offline",
440 data.token,
441 error=True
442 )
443 # use request timeout style response for offline status
444 try:
445 return HCI.request_timeout(bod)
446 except AttributeError:
447 return HCI.not_found(bod)
448
449 ip_address = latest.get("ip", "")
450 if not ip_address:
451 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
452
453 # try to query the feeder device with a short timeout
454 try:
455 url = f"http://{ip_address}"
456 resp = requests.get(url, timeout=5)
457 if 200 <= resp.status_code < 400:
458 bod = self.boilerplate_responses_initialised.build_response_body(
459 title,
460 "Feeder is reachable",
461 "reachable",
462 data.token,
463 error=False
464 )
465 return HCI.success(bod)
466 bod = self.boilerplate_responses_initialised.build_response_body(
467 title,
468 f"Feeder responded with status {resp.status_code}",
469 "unreachable",
470 data.token,
471 error=True
472 )
473 return HCI.bad_gateway(bod) if hasattr(HCI, "bad_gateway") else HCI.internal_server_error(bod)
474 except requests.exceptions.Timeout:
475 bod = self.boilerplate_responses_initialised.build_response_body(
476 title,
477 "Timed out while contacting feeder",
478 "timeout",
479 data.token,
480 error=True
481 )
482 return HCI.request_timeout(bod) if hasattr(HCI, "request_timeout") else HCI.internal_server_error(bod)
483 except (requests.exceptions.ConnectionError, requests.exceptions.RequestException):
484 bod = self.boilerplate_responses_initialised.build_response_body(
485 title,
486 "Error while contacting feeder",
487 "error",
488 data.token,
489 error=True
490 )
491 return HCI.internal_server_error(bod)
492
493 async def put_feeder_ip(self, request: Request) -> Response:
494 """Update the IP address of the feeder (called by feeder itself)
495
496 Args:
497 request (Request): The incoming request with MAC and new IP
498
499 Returns:
500 Response: Success/error response
501 """
502 title = "put_feeder_ip"
503
504 body = await self.boilerplate_incoming_initialised.get_body(request)
505
506 # Require MAC and new IP address
507 required_fields = ["mac", "ip"]
508 for field in required_fields:
509 if field not in body:
510 return self.boilerplate_responses_initialised.missing_variable_in_body(
511 title, "", field
512 )
513
514 feeder_rows = self.database_link.get_data_from_table(
515 self.tab_feeder,
516 ["id"],
517 f"mac='{body['mac']}'",
518 beautify=True
519 )
520
521 if not isinstance(feeder_rows, list) or len(feeder_rows) == 0:
522 return HCI.not_found(
523 self.boilerplate_responses_initialised.build_response_body(
524 title,
525 "Feeder not found with this MAC",
526 "not_found",
527 "",
528 error=True
529 )
530 )
531
532 feeder_id = feeder_rows[0]["id"]
533 new_ip = body["ip"]
534
535 # Insert or update IP record for this feeder
536 cols = self.database_link.get_table_column_names(self.tab_feeder_ip)
537 if not isinstance(cols, list):
538 return self.boilerplate_responses_initialised.internal_server_error(title, "")
539
540 cols = CONST.clean_list(cols, self.cols_to_remove, self.disp)
541
542 # Try to update existing record first
543 existing_ip = self.database_link.get_data_from_table(
544 self.tab_feeder_ip,
545 ["id"],
546 f"parent_id={feeder_id}",
547 beautify=True
548 )
549
550 if isinstance(existing_ip, list) and len(existing_ip) > 0:
551 self.disp.log_debug(f"Existing IP record found: {existing_ip}")
552 self.disp.log_debug(f"Updating IP to: {new_ip}")
553 # Update existing record
554 _now = datetime.now() # tz=self.forced_timezone
555 now_str = self.database_link.datetime_to_string(_now, False, True)
556 resp = self.database_link.update_data_in_table(
557 self.tab_feeder_ip,
558 [new_ip, now_str],
559 ["ip", "edit_date"],
560 where=f"parent_id={feeder_id}"
561 )
562 else:
563 self.disp.log_debug(
564 "No existing IP record found, inserting new one.")
565 self.disp.log_debug(f"Inserting new IP: {new_ip}")
566 # Insert new record
567 sql_data = [feeder_id, new_ip]
568 resp = self.database_link.insert_data_into_table(
569 self.tab_feeder_ip,
570 sql_data,
571 cols
572 )
573
574 if resp == self.database_link.error:
575 return self.boilerplate_responses_initialised.internal_server_error(title, "")
576
577 bod = self.boilerplate_responses_initialised.build_response_body(
578 title,
579 f"Feeder IP updated to {new_ip}",
580 "updated",
581 "",
582 error=False
583 )
584 return HCI.success(bod)
585
586 async def delete_feeder(self, request: Request) -> Response:
587 """Delete a cat feeder from the database.
588
589 Args:
590 request (Request): The incoming request parameters.
591 Returns:
592 Response: The HTTP response to send back to the user.
593 """
594 title = "delete_feeder"
595 data = self._user_connected(request, title)
596 if isinstance(data, Response):
597 return data
598 body = await self.boilerplate_incoming_initialised.get_body(request)
599 if "id" not in body and "mac" not in body:
600 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id or mac")
601
602 if "id" in body:
603 try:
604 feeder_id_val = int(body["id"])
605 except (ValueError, TypeError):
606 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "valid id")
607 where = f"owner={data.user_id} AND id={feeder_id_val}"
608 else:
609 where = f"owner={data.user_id} AND mac='{body['mac']}'"
610
611 # Check if feeder exists and belongs to user
612 feeder_rows = self.database_link.get_data_from_table(
613 self.tab_feeder,
614 ["id"],
615 where,
616 beautify=True
617 )
618 if not isinstance(feeder_rows, list) or len(feeder_rows) == 0:
619 return HCI.not_found(
620 self.boilerplate_responses_initialised.build_response_body(
621 title,
622 "Feeder not found or not owned by user",
623 "not_found",
624 data.token,
625 error=True
626 )
627 )
628
629 feeder_id = feeder_rows[0]["id"]
630
631 resp = self.database_link.remove_data_from_table(
632 self.tab_feeder,
633 f"id={feeder_id} AND owner={data.user_id}"
634 )
635 if resp == self.database_link.error:
636 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
637
638 bod = self.boilerplate_responses_initialised.build_response_body(
639 title, "Feeder deleted successfully", "deleted", data.token, error=False
640 )
641 return HCI.success(bod)
642
643 async def get_feeders(self, request: Request) -> Response:
644 """Get all feeders for the authenticated user.
645
646 Args:
647 request (Request): The incoming request parameters.
648
649 Returns:
650 Response: The HTTP response with the list of feeders.
651 """
652 title = "get_feeders"
653 data = self._user_connected(request, title)
654 if isinstance(data, Response):
655 return data
656 feeders = self.database_link.get_data_from_table(
657 self.tab_feeder,
658 [
659 "id", "name", "mac", "latitude", "longitude",
660 "city_locality", "country", "creation_date", "edit_date"
661 ],
662 f"owner={data.user_id}",
663 beautify=True
664 )
665 if not isinstance(feeders, list):
666 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
667 feeders = EN_CONST.sanitize_response_data(
668 feeders, disp=self.disp
669 )
670 bod = self.boilerplate_responses_initialised.build_response_body(
671 title, "The feeders have been gathered", feeders, data.token, error=False
672 )
673 return HCI.success(bod)
674
675 async def put_register_beacon(self, request: Request) -> Response:
676 """Register a beacon signal from a cat feeder.
677
678 Args:
679 request (Request): The incoming request parameters.
680 Returns:
681 Response: The HTTP response to send back to the user.
682 """
683 title = "register_beacon"
684 data = self._user_connected(request, title)
685 if isinstance(data, Response):
686 return data
687 body = await self.boilerplate_incoming_initialised.get_body(request)
688 elems = ["mac", "name"]
689 for elem in elems:
690 if elem not in body:
691 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, elem)
692 self.disp.log_debug(f"body: {body}")
693
694 # Check if beacon already exists with this MAC or name
695 present = self.database_link.get_data_from_table(
696 self.tab_beacon,
697 ["id"],
698 f"owner={data.user_id} AND (mac='{body['mac']}' OR name='{body['name']}')",
699 beautify=True
700 )
701 self.disp.log_debug(f"present: {present}")
702 if isinstance(present, list) and len(present) > 0:
703 return HCI.conflict(
704 self.boilerplate_responses_initialised.build_response_body(
705 title,
706 "Beacon with this MAC address or name already registered",
707 "exists",
708 data.token,
709 error=True
710 )
711 )
712
713 cols = self.database_link.get_table_column_names(self.tab_beacon)
714 self.disp.log_debug(f"Column names: {cols}")
715 if not isinstance(cols, list):
716 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
717 cols = CONST.clean_list(cols, self.cols_to_remove, self.disp)
718 sql_data = [str(data.user_id), body["mac"], body["name"]]
719 self.disp.log_debug(f"raw sql_data: {sql_data}")
720 self.disp.log_debug(f"cols: {cols}")
721 resp = self.database_link.insert_data_into_table(
722 self.tab_beacon,
723 sql_data,
724 cols
725 )
726 if resp == self.database_link.error:
727 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
728
729 bod = self.boilerplate_responses_initialised.build_response_body(
730 title, "Beacon registered successfully", "registered", data.token, error=False
731 )
732 return HCI.created(bod)
733
734 async def get_beacon_status(self, request: Request) -> Response:
735 """Get the status of a beacon signal from a cat feeder.
736
737 Args:
738 request (Request): The incoming request parameters.
739 Returns:
740 Response: The HTTP response to send back to the user.
741 """
742 title = "get_beacon_status"
743 data = self._user_connected(request, title)
744 if isinstance(data, Response):
745 return data
746 body = await self.boilerplate_incoming_initialised.get_body(request)
747 # Accept beacon identifier: id or name or mac
748 if not body or ("id" not in body and "name" not in body and "mac" not in body):
749 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id or name or mac")
750
751 beacon_id = None
752 if "id" in body:
753 try:
754 beacon_id = int(body["id"])
755 except (ValueError, TypeError):
756 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "valid id")
757 else:
758 # resolve by name or mac
759 if "name" in body:
760 where = f"owner={data.user_id} AND name='{body['name']}'"
761 else:
762 where = f"owner={data.user_id} AND mac='{body['mac']}'"
763 rows = self.database_link.get_data_from_table(
764 self.tab_beacon,
765 ["id"],
766 where,
767 beautify=True
768 )
769 if not isinstance(rows, list) or len(rows) == 0:
770 return HCI.not_found(
771 self.boilerplate_responses_initialised.build_response_body(
772 title,
773 "Beacon not found",
774 "not_found",
775 data.token,
776 error=True
777 )
778 )
779 beacon_id = rows[0]["id"]
780
781 beacon_data = self.database_link.get_data_from_table(
782 self.tab_beacon,
783 ["id", "mac", "name", "creation_date", "edit_date"],
784 f"id={beacon_id} AND owner={data.user_id}",
785 beautify=True
786 )
787 if not isinstance(beacon_data, list) or len(beacon_data) == 0:
788 return HCI.not_found(
789 self.boilerplate_responses_initialised.build_response_body(
790 title,
791 "Beacon not found",
792 "not_found",
793 data.token,
794 error=True
795 )
796 )
797 beacon_cleared = EN_CONST.sanitize_response_data(
798 beacon_data[0], disp=self.disp
799 )
800 bod = self.boilerplate_responses_initialised.build_response_body(
801 title,
802 "Beacon status retrieved successfully",
803 resp={"beacon": beacon_cleared},
804 token=data.token,
805 error=False,
806 )
807 return HCI.success(bod)
808
809 async def patch_beacon(self, request: Request) -> Response:
810 """Update the beacon signal from a cat feeder.
811
812 Args:
813 request (Request): The incoming request parameters.
814 Returns:
815 Response: The HTTP response to send back to the user.
816 """
817 title = "patch_beacon"
818 data = self._user_connected(request, title)
819 if isinstance(data, Response):
820 return data
821 body = await self.boilerplate_incoming_initialised.get_body(request)
822
823 # identifier: either id or mac must be provided
824 if "id" not in body and "mac" not in body:
825 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id or mac")
826
827 # allowed fields to update
828 allowed = {"mac", "name"}
829
830 cols = self.database_link.get_table_column_names(self.tab_beacon)
831 if not isinstance(cols, list):
832 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
833
834 # determine which columns from the table we can update based on request body
835 update_cols = []
836 for c in cols:
837 if c in allowed and c in body:
838 update_cols.append(c)
839 if not update_cols:
840 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "fields to update")
841
842 sql_data = []
843 for col in update_cols:
844 sql_data.append(body[col])
845
846 # build where clause to ensure user updates only their beacon
847 if "id" in body:
848 try:
849 beacon_id = int(body["id"])
850 except (ValueError, TypeError):
851 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "valid id")
852 where = f"id={beacon_id} AND owner={data.user_id}"
853 else:
854 where = f"owner={data.user_id} AND mac='{body['mac']}'"
855
856 resp = self.database_link.update_data_in_table(
857 self.tab_beacon,
858 sql_data,
859 update_cols,
860 where=where
861 )
862 if resp == self.database_link.error:
863 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
864
865 bod = self.boilerplate_responses_initialised.build_response_body(
866 title, "Beacon updated successfully", "updated", data.token, error=False
867 )
868 return HCI.success(bod)
869
870 async def delete_beacon(self, request: Request) -> Response:
871 """Delete the beacon signal from a cat feeder.
872
873 Args:
874 request (Request): The incoming request parameters.
875 Returns:
876 Response: The HTTP response to send back to the user.
877 """
878 title = "delete_beacon"
879 data = self._user_connected(request, title)
880 if isinstance(data, Response):
881 return data
882 body = await self.boilerplate_incoming_initialised.get_body(request)
883 elem = "id"
884 if elem not in body:
885 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, elem)
886
887 try:
888 beacon_id = int(body["id"])
889 except (ValueError, TypeError):
890 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "valid id")
891
892 # Check if beacon exists and belongs to user
893 beacon_exists = self.database_link.get_data_from_table(
894 self.tab_beacon,
895 ["id"],
896 f"id={beacon_id} AND owner={data.user_id}",
897 beautify=True
898 )
899 if not isinstance(beacon_exists, list) or len(beacon_exists) == 0:
900 return HCI.not_found(
901 self.boilerplate_responses_initialised.build_response_body(
902 title,
903 "Beacon not found or not owned by user",
904 "not_found",
905 data.token,
906 error=True
907 )
908 )
909
910 resp = self.database_link.remove_data_from_table(
911 self.tab_beacon,
912 f"id={beacon_id} AND owner={data.user_id}"
913 )
914 if resp == self.database_link.error:
915 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
916
917 bod = self.boilerplate_responses_initialised.build_response_body(
918 title, "Beacon deleted successfully", "deleted", data.token, error=False
919 )
920 return HCI.success(bod)
921
922 async def get_beacons(self, request: Request) -> Response:
923 """Get all beacons for the authenticated user.
924
925 Args:
926 request (Request): The incoming request parameters.
927
928 Returns:
929 Response: The HTTP response with the list of beacons.
930 """
931 title = "get_beacons"
932 data = self._user_connected(request, title)
933 if isinstance(data, Response):
934 return data
935 beacons_raw = self.database_link.get_data_from_table(
936 self.tab_beacon,
937 ["id", "name", "mac", "creation_date", "edit_date"],
938 f"owner={data.user_id}",
939 beautify=True
940 )
941 if not isinstance(beacons_raw, list):
942 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
943
944 beacons = EN_CONST.sanitize_response_data(
945 beacons_raw, disp=self.disp
946 )
947
948 bod = self.boilerplate_responses_initialised.build_response_body(
949 title, "The beacons have been gathered", beacons, data.token, error=False
950 )
951 return HCI.success(bod)
952
953 async def get_beacon_locations(self, request: Request) -> Response:
954 """Get the list of beacon locations from cat feeders.
955
956 Args:
957 request (Request): The incoming request parameters.
958 Returns:
959 Response: The HTTP response to send back to the user.
960 """
961 title = "get_beacon_locations"
962 data = self._user_connected(request, title)
963 if isinstance(data, Response):
964 return data
965 body = await self.boilerplate_incoming_initialised.get_body(request)
966
967 # Require beacon identifier (name or mac)
968 if "name" not in body and "mac" not in body:
969 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "name or mac")
970
971 # Get beacon ID
972 if "name" in body:
973 where_clause = f"owner={data.user_id} AND name='{body['name']}'"
974 else:
975 where_clause = f"owner={data.user_id} AND mac='{body['mac']}'"
976
977 beacon_data = self.database_link.get_data_from_table(
978 self.tab_beacon,
979 ["id"],
980 where_clause,
981 beautify=True
982 )
983 if not isinstance(beacon_data, list) or len(beacon_data) == 0:
984 return HCI.not_found(
985 self.boilerplate_responses_initialised.build_response_body(
986 title,
987 "Beacon not found",
988 "not_found",
989 data.token,
990 error=True
991 )
992 )
993
994 beacon_id = beacon_data[0]["id"]
995
996 # Get location history for this beacon
997 location_data = self.database_link.get_data_from_table(
999 ["feeder", "creation_date"],
1000 f"beacon={beacon_id}",
1001 beautify=True
1002 )
1003 if not isinstance(location_data, list):
1004 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1005
1006 # Correlate with feeder data
1007 locations = []
1008 for location in location_data:
1009 feeder_data = self.database_link.get_data_from_table(
1010 self.tab_feeder,
1011 ["name", "latitude", "longitude", "city_locality", "country"],
1012 f"id={location['feeder']} AND owner={data.user_id}",
1013 beautify=True
1014 )
1015 if isinstance(feeder_data, list) and len(feeder_data) > 0:
1016 locations.append(
1017 {
1018 "visit_time": location["creation_date"],
1019 "feeder": feeder_data[0]
1020 }
1021 )
1022
1023 locations_cleaned = EN_CONST.sanitize_response_data(
1024 locations, disp=self.disp
1025 )
1026 bod = self.boilerplate_responses_initialised.build_response_body(
1027 title,
1028 "Beacon locations retrieved successfully",
1029 {"locations": locations_cleaned},
1030 data.token,
1031 error=False
1032 )
1033 return HCI.success(bod)
1034
1035 async def post_beacon_location(self, request: Request) -> Response:
1036 """Post a new beacon location from a cat feeder.
1037
1038 Args:
1039 request (Request): The incoming request parameters.
1040 Returns:
1041 Response: The HTTP response to send back to the user.
1042 """
1043 title = "post_beacon_location"
1044 body = await self.boilerplate_incoming_initialised.get_body(request)
1045
1046 # Require both beacon and feeder MACs
1047 elems = ["beacon_mac", "feeder_mac"]
1048 for elem in elems:
1049 if elem not in body:
1050 return self.boilerplate_responses_initialised.missing_variable_in_body(title, "", elem)
1051
1052 # Get beacon ID
1053 beacon_data = self.database_link.get_data_from_table(
1054 self.tab_beacon,
1055 ["id"],
1056 f"mac='{body['beacon_mac']}'",
1057 beautify=True
1058 )
1059 if not isinstance(beacon_data, list) or len(beacon_data) == 0:
1060 return HCI.not_found(
1061 self.boilerplate_responses_initialised.build_response_body(
1062 title,
1063 "Beacon not found",
1064 "not_found",
1065 "",
1066 error=True
1067 )
1068 )
1069
1070 # Get feeder ID
1071 feeder_data = self.database_link.get_data_from_table(
1072 self.tab_feeder,
1073 ["id"],
1074 f"mac='{body['feeder_mac']}'",
1075 beautify=True
1076 )
1077 if not isinstance(feeder_data, list) or len(feeder_data) == 0:
1078 return HCI.not_found(
1079 self.boilerplate_responses_initialised.build_response_body(
1080 title,
1081 "Feeder not found",
1082 "not_found",
1083 "",
1084 error=True
1085 )
1086 )
1087
1088 beacon_id = beacon_data[0]["id"]
1089 feeder_id = feeder_data[0]["id"]
1090
1091 # Insert location record
1092 cols = self.database_link.get_table_column_names(
1094 if not isinstance(cols, list):
1095 return self.boilerplate_responses_initialised.internal_server_error(title, "")
1096 cols = CONST.clean_list(cols, self.cols_to_remove, self.disp)
1097 sql_data = [beacon_id, feeder_id]
1098
1099 resp = self.database_link.insert_data_into_table(
1101 sql_data,
1102 cols
1103 )
1104 if resp == self.database_link.error:
1105 return self.boilerplate_responses_initialised.internal_server_error(title, "")
1106
1107 bod = self.boilerplate_responses_initialised.build_response_body(
1108 title, "Beacon location recorded successfully", "recorded", "", error=False
1109 )
1110 return HCI.created(bod)
1111
1112 async def get_feeder_visits(self, request: Request) -> Response:
1113 """Get the list of visits recorded by a cat feeder.
1114
1115 Args:
1116 request (Request): The incoming request parameters.
1117 Returns:
1118 Response: The HTTP response to send back to the user.
1119 """
1120 title = "get_feeder_visits"
1121 data = self._user_connected(request, title)
1122 if isinstance(data, Response):
1123 return data
1124 body = await self.boilerplate_incoming_initialised.get_body(request)
1125
1126 # Require feeder identifier (name or mac)
1127 if "name" not in body and "mac" not in body:
1128 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "name or mac")
1129
1130 # Get feeder ID
1131 if "name" in body:
1132 where_clause = f"owner={data.user_id} AND name='{body['name']}'"
1133 else:
1134 where_clause = f"owner={data.user_id} AND mac='{body['mac']}'"
1135
1136 feeder_data = self.database_link.get_data_from_table(
1137 self.tab_feeder,
1138 ["id"],
1139 where_clause,
1140 beautify=True
1141 )
1142 if not isinstance(feeder_data, list) or len(feeder_data) == 0:
1143 return HCI.not_found(
1144 self.boilerplate_responses_initialised.build_response_body(
1145 title,
1146 "Feeder not found",
1147 "not_found",
1148 data.token,
1149 error=True
1150 )
1151 )
1152
1153 feeder_id = feeder_data[0]["id"]
1154
1155 # Get visits for this feeder
1156 visit_data = self.database_link.get_data_from_table(
1158 ["beacon", "creation_date"],
1159 f"feeder={feeder_id}",
1160 beautify=True
1161 )
1162 if not isinstance(visit_data, list):
1163 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1164
1165 # Get beacon information for each visit
1166 visits_raw = []
1167 for visit in visit_data:
1168 beacon_info = self.database_link.get_data_from_table(
1169 self.tab_beacon,
1170 ["mac", "name"],
1171 f"id={visit['beacon']}",
1172 beautify=True
1173 )
1174 if isinstance(beacon_info, list) and len(beacon_info) > 0:
1175 visits_raw.append({
1176 "visit_time": visit["creation_date"],
1177 "beacon": beacon_info[0]
1178 })
1179
1180 visits = EN_CONST.sanitize_response_data(
1181 visits_raw, disp=self.disp
1182 )
1183 bod = self.boilerplate_responses_initialised.build_response_body(
1184 title,
1185 "Feeder visits retrieved successfully",
1186 {"visits": visits},
1187 data.token,
1188 error=False
1189 )
1190 return HCI.success(bod)
1191
1192 async def get_distribute_food(self, request: Request) -> Response:
1193 """Get the food distribution status from a cat feeder.
1194
1195 Args:
1196 request (Request): The incoming request parameters.
1197 Returns:
1198 Response: The HTTP response to send back to the user.
1199 """
1200 title = "get_distribute_food"
1201 body = await self.boilerplate_incoming_initialised.get_body(request)
1202
1203 # Require beacon MAC to identify the pet
1204 if "beacon_mac" not in body:
1205 return self.boilerplate_responses_initialised.missing_variable_in_body(title, "", "beacon_mac")
1206
1207 # Get beacon ID
1208 beacon_data = self.database_link.get_data_from_table(
1209 self.tab_beacon,
1210 ["id"],
1211 f"mac='{body['beacon_mac']}'",
1212 beautify=True
1213 )
1214 if not isinstance(beacon_data, list) or len(beacon_data) == 0:
1215 return HCI.not_found(
1216 self.boilerplate_responses_initialised.build_response_body(
1217 title,
1218 "Beacon not found",
1219 "not_found",
1220 "",
1221 error=True
1222 )
1223 )
1224
1225 beacon_id = beacon_data[0]["id"]
1226
1227 # Get pet data for this beacon
1228 pet_data = self.database_link.get_data_from_table(
1229 self.tab_pet,
1230 ["food_eaten", "food_max", "food_reset",
1231 "time_reset_hours", "time_reset_minutes"],
1232 f"beacon={beacon_id}",
1233 beautify=True
1234 )
1235 if not isinstance(pet_data, list) or len(pet_data) == 0:
1236 return HCI.not_found(
1237 self.boilerplate_responses_initialised.build_response_body(
1238 title,
1239 "Pet not found for this beacon",
1240 "not_found",
1241 "",
1242 error=True
1243 )
1244 )
1245
1246 pet = pet_data[0]
1247
1248 # Check if food counter needs reset
1249 food_reset_time = self._parse_dt(pet.get("food_reset"))
1250 if food_reset_time and datetime.now(timezone.utc) >= food_reset_time:
1251 # Reset food counter and update next reset time
1252 reset_hours = pet.get("time_reset_hours", 24)
1253 reset_minutes = pet.get("time_reset_minutes", 0)
1254 next_reset = datetime.now(
1255 timezone.utc) + timedelta(hours=reset_hours, minutes=reset_minutes)
1256
1257 self.database_link.update_data_in_table(
1258 self.tab_pet,
1259 [0, next_reset.isoformat()],
1260 ["food_eaten", "food_reset"],
1261 where=f"beacon={beacon_id}"
1262 )
1263 pet["food_eaten"] = 0
1264
1265 # Check if pet can receive food
1266 can_distribute = pet["food_eaten"] < pet["food_max"]
1267
1268 raw_content = {
1269 "can_distribute": can_distribute,
1270 "food_eaten": pet["food_eaten"],
1271 "food_max": pet["food_max"]
1272 }
1273
1274 cleaned_content = EN_CONST.sanitize_response_data(
1275 raw_content, disp=self.disp
1276 )
1277
1278 bod = self.boilerplate_responses_initialised.build_response_body(
1279 title,
1280 "Food distribution status checked",
1281 cleaned_content,
1282 "",
1283 error=False
1284 )
1285 return HCI.success(bod)
1286
1287 async def post_distribute_food(self, request: Request) -> Response:
1288 """Record food distribution to a pet.
1289
1290 Args:
1291 request (Request): The incoming request parameters.
1292 Returns:
1293 Response: The HTTP response to send back to the user.
1294 """
1295 title = "post_distribute_food"
1296 body = await self.boilerplate_incoming_initialised.get_body(request)
1297
1298 # Require both beacon and feeder MACs
1299 elems = ["beacon_mac", "feeder_mac"]
1300 for elem in elems:
1301 if elem not in body:
1302 return self.boilerplate_responses_initialised.missing_variable_in_body(title, "", elem)
1303
1304 # Get beacon ID
1305 beacon_data = self.database_link.get_data_from_table(
1306 self.tab_beacon,
1307 ["id"],
1308 f"mac='{body['beacon_mac']}'",
1309 beautify=True
1310 )
1311 if not isinstance(beacon_data, list) or len(beacon_data) == 0:
1312 return HCI.not_found(
1313 self.boilerplate_responses_initialised.build_response_body(
1314 title,
1315 "Beacon not found",
1316 "not_found",
1317 "",
1318 error=True
1319 )
1320 )
1321
1322 # Get feeder ID (verify feeder exists)
1323 feeder_data = self.database_link.get_data_from_table(
1324 self.tab_feeder,
1325 ["id"],
1326 f"mac='{body['feeder_mac']}'",
1327 beautify=True
1328 )
1329 if not isinstance(feeder_data, list) or len(feeder_data) == 0:
1330 return HCI.not_found(
1331 self.boilerplate_responses_initialised.build_response_body(
1332 title,
1333 "Feeder not found",
1334 "not_found",
1335 "",
1336 error=True
1337 )
1338 )
1339
1340 beacon_id = beacon_data[0]["id"]
1341
1342 # Get pet data
1343 pet_data = self.database_link.get_data_from_table(
1344 self.tab_pet,
1345 [
1346 "food_eaten", "food_max", "food_reset",
1347 "time_reset_hours", "time_reset_minutes"
1348 ],
1349 f"beacon={beacon_id}",
1350 beautify=True
1351 )
1352 if not isinstance(pet_data, list) or len(pet_data) == 0:
1353 return HCI.not_found(
1354 self.boilerplate_responses_initialised.build_response_body(
1355 title,
1356 "Pet not found for this beacon",
1357 "not_found",
1358 "",
1359 error=True
1360 )
1361 )
1362
1363 pet = pet_data[0]
1364
1365 # Check if food counter needs reset
1366 food_reset_time = self._parse_dt(pet.get("food_reset"))
1367 if food_reset_time and datetime.now(timezone.utc) >= food_reset_time:
1368 # Reset food counter
1369 reset_hours = pet.get("time_reset_hours", 24)
1370 reset_minutes = pet.get("time_reset_minutes", 0)
1371 next_reset = datetime.now(
1372 timezone.utc) + timedelta(hours=reset_hours, minutes=reset_minutes)
1373
1374 self.database_link.update_data_in_table(
1375 self.tab_pet,
1376 [0, next_reset.isoformat()],
1377 ["food_eaten", "food_reset"],
1378 where=f"beacon={beacon_id}"
1379 )
1380 pet["food_eaten"] = 0
1381
1382 # Check if pet can receive food
1383 if pet["food_eaten"] >= pet["food_max"]:
1384 return HCI.forbidden(
1385 self.boilerplate_responses_initialised.build_response_body(
1386 title,
1387 "Pet has reached daily food limit",
1388 "limit_reached",
1389 "",
1390 error=True
1391 )
1392 )
1393
1394 # Distribute food (increment counter)
1395 food_amount = body.get("amount", 1)
1396 new_food_eaten = pet["food_eaten"] + food_amount
1397
1398 # Ensure we don't exceed the limit
1399 if new_food_eaten > pet["food_max"]:
1400 new_food_eaten = pet["food_max"]
1401
1402 # Update pet food counter
1403 resp = self.database_link.update_data_in_table(
1404 self.tab_pet,
1405 [new_food_eaten],
1406 ["food_eaten"],
1407 where=f"beacon={beacon_id}"
1408 )
1409 if resp == self.database_link.error:
1410 return self.boilerplate_responses_initialised.internal_server_error(title, "")
1411
1412 raw_data = {
1413 "amount_distributed": food_amount,
1414 "new_total": new_food_eaten,
1415 "remaining": pet["food_max"] - new_food_eaten
1416 }
1417 cleaned_data = EN_CONST.sanitize_response_data(
1418 raw_data, disp=self.disp
1419 )
1420
1421 bod = self.boilerplate_responses_initialised.build_response_body(
1422 title,
1423 "Food distributed successfully",
1424 cleaned_data,
1425 "",
1426 error=False
1427 )
1428 return HCI.success(bod)
1429
1430 async def post_feeder_visit(self, request: Request) -> Response:
1431 """Register a visit from a cat to a feeder.
1432
1433 Args:
1434 request (Request): The incoming request parameters.
1435 Returns:
1436 Response: The HTTP response to send back to the user.
1437 """
1438 title = "post_feeder_visit"
1439 body = await self.boilerplate_incoming_initialised.get_body(request)
1440
1441 # This endpoint might be called with different parameter combinations
1442 # It could be called with feeder_mac + beacon_mac, or just beacon_mac if the feeder is making the call
1443 if "beacon_mac" not in body:
1444 return self.boilerplate_responses_initialised.missing_variable_in_body(title, "", "beacon_mac")
1445
1446 # If feeder_mac is not provided, we might need to identify the feeder differently
1447 # For example, by IP address or require it in the request
1448 if "feeder_mac" not in body:
1449 return self.boilerplate_responses_initialised.missing_variable_in_body(title, "", "feeder_mac")
1450
1451 # Get beacon ID
1452 beacon_data = self.database_link.get_data_from_table(
1453 self.tab_beacon,
1454 ["id"],
1455 f"mac='{body['beacon_mac']}'",
1456 beautify=True
1457 )
1458 if not isinstance(beacon_data, list) or len(beacon_data) == 0:
1459 return HCI.not_found(
1460 self.boilerplate_responses_initialised.build_response_body(
1461 title,
1462 "Beacon not found",
1463 "not_found",
1464 "",
1465 error=True
1466 )
1467 )
1468
1469 # Get feeder ID
1470 feeder_data = self.database_link.get_data_from_table(
1471 self.tab_feeder,
1472 ["id"],
1473 f"mac='{body['feeder_mac']}'",
1474 beautify=True
1475 )
1476 if not isinstance(feeder_data, list) or len(feeder_data) == 0:
1477 return HCI.not_found(
1478 self.boilerplate_responses_initialised.build_response_body(
1479 title,
1480 "Feeder not found",
1481 "not_found",
1482 "",
1483 error=True
1484 )
1485 )
1486
1487 beacon_id = beacon_data[0]["id"]
1488 feeder_id = feeder_data[0]["id"]
1489
1490 # Insert visit record in location history
1491 cols = self.database_link.get_table_column_names(
1493 if not isinstance(cols, list):
1494 return self.boilerplate_responses_initialised.internal_server_error(title, "")
1495 cols = CONST.clean_list(cols, self.cols_to_remove, self.disp)
1496 sql_data = [beacon_id, feeder_id]
1497
1498 resp = self.database_link.insert_data_into_table(
1500 sql_data,
1501 cols
1502 )
1503 if resp == self.database_link.error:
1504 return self.boilerplate_responses_initialised.internal_server_error(title, "")
1505
1506 bod = self.boilerplate_responses_initialised.build_response_body(
1507 title, "Feeder visit recorded successfully", "recorded", "", error=False
1508 )
1509 return HCI.created(bod)
1510
1511 async def put_register_pet(self, request: Request) -> Response:
1512 """Register a new pet linked to a beacon.
1513
1514 Args:
1515 request (Request): The incoming request parameters.
1516 Returns:
1517 Response: The HTTP response to send back to the user.
1518 """
1519 title = "put_register_pet"
1520 data = self._user_connected(request, title)
1521 if isinstance(data, Response):
1522 return data
1523 body = await self.boilerplate_incoming_initialised.get_body(request)
1524 # Accept either `beacon_id` (numeric) or `beacon_mac` (string) to identify the beacon
1525 if "name" not in body:
1526 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "name")
1527
1528 beacon_id = None
1529 if "beacon_id" in body:
1530 try:
1531 beacon_id = int(body["beacon_id"])
1532 except (ValueError, TypeError):
1533 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "valid beacon_id")
1534 elif "beacon_mac" in body:
1535 # resolve mac to id
1536 beacon_rows = self.database_link.get_data_from_table(
1537 self.tab_beacon,
1538 ["id"],
1539 f"mac='{body['beacon_mac']}' AND owner={data.user_id}",
1540 beautify=True
1541 )
1542 if not isinstance(beacon_rows, list) or len(beacon_rows) == 0:
1543 return HCI.not_found(
1544 self.boilerplate_responses_initialised.build_response_body(
1545 title,
1546 "Beacon not found or not owned by user",
1547 "not_found",
1548 data.token,
1549 error=True
1550 )
1551 )
1552 beacon_id = beacon_rows[0]["id"]
1553 else:
1554 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "beacon_id or beacon_mac")
1555
1556 # Check if beacon exists and belongs to user (redundant for beacon_mac path but kept for beacon_id path)
1557 beacon_exists = self.database_link.get_data_from_table(
1558 self.tab_beacon,
1559 ["id"],
1560 f"id={beacon_id} AND owner={data.user_id}",
1561 beautify=True
1562 )
1563 if not isinstance(beacon_exists, list) or len(beacon_exists) == 0:
1564 return HCI.not_found(
1565 self.boilerplate_responses_initialised.build_response_body(
1566 title,
1567 "Beacon not found or not owned by user",
1568 "not_found",
1569 data.token,
1570 error=True
1571 )
1572 )
1573
1574 # Check if pet already exists for this beacon
1575 present = self.database_link.get_data_from_table(
1576 self.tab_pet,
1577 ["id"],
1578 f"beacon={beacon_id}",
1579 beautify=True
1580 )
1581 if isinstance(present, list) and len(present) > 0:
1582 return HCI.conflict(
1583 self.boilerplate_responses_initialised.build_response_body(
1584 title,
1585 "Pet already registered for this beacon",
1586 "exists",
1587 data.token,
1588 error=True
1589 )
1590 )
1591
1592 cols = self.database_link.get_table_column_names(self.tab_pet)
1593 if not isinstance(cols, list):
1594 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1595 cols = CONST.clean_list(cols, self.cols_to_remove, self.disp)
1596
1597 time_reset_hours = body.get("time_reset_hours", 24)
1598 time_reset_minutes = body.get("time_reset_minutes", 0)
1599 default_reset: datetime = datetime.now(timezone.utc).astimezone()
1600 default_reset = default_reset + timedelta(
1601 hours=time_reset_hours,
1602 minutes=time_reset_minutes
1603 )
1604
1605 sql_data = [
1606 beacon_id,
1607 body["name"],
1608 body.get("breed", None),
1609 body.get("age", None),
1610 body.get("weight", None),
1611 body.get("microchip_id", None),
1612 body.get("food_eaten", 0),
1613 body.get("food_max", 100),
1614 body.get("food_reset", default_reset),
1615 time_reset_hours,
1616 time_reset_minutes
1617 ]
1618
1619 resp = self.database_link.insert_data_into_table(
1620 self.tab_pet,
1621 sql_data,
1622 cols
1623 )
1624 if resp == self.database_link.error:
1625 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1626
1627 bod = self.boilerplate_responses_initialised.build_response_body(
1628 title, "Pet registered successfully", "registered", data.token, error=False
1629 )
1630 return HCI.created(bod)
1631
1632 async def patch_pet(self, request: Request) -> Response:
1633 """Update pet information.
1634
1635 Args:
1636 request (Request): The incoming request parameters.
1637 Returns:
1638 Response: The HTTP response to send back to the user.
1639 """
1640 title = "patch_pet"
1641 data = self._user_connected(request, title)
1642 if isinstance(data, Response):
1643 return data
1644 body = await self.boilerplate_incoming_initialised.get_body(request)
1645
1646 if "id" not in body:
1647 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id")
1648
1649 try:
1650 pet_id = int(body["id"])
1651 except (ValueError, TypeError):
1652 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "valid id")
1653
1654 # Check if pet exists and beacon belongs to user
1655 pet_data = self.database_link.get_data_from_table(
1656 self.tab_pet,
1657 ["beacon"],
1658 f"id={pet_id}",
1659 beautify=True
1660 )
1661 if not isinstance(pet_data, list) or len(pet_data) == 0:
1662 return HCI.not_found(
1663 self.boilerplate_responses_initialised.build_response_body(
1664 title,
1665 "Pet not found",
1666 "not_found",
1667 data.token,
1668 error=True
1669 )
1670 )
1671
1672 beacon_id = pet_data[0]["beacon"]
1673 beacon_owner = self.database_link.get_data_from_table(
1674 self.tab_beacon,
1675 ["owner"],
1676 f"id={beacon_id}",
1677 beautify=True
1678 )
1679 if not isinstance(beacon_owner, list) or len(beacon_owner) == 0 or beacon_owner[0]["owner"] != data.user_id:
1680 return self.boilerplate_responses_initialised.insuffisant_rights(title, data.token)
1681
1682 # allowed fields to update
1683 allowed = {
1684 "name", "food_eaten", "food_max", "food_reset",
1685 "time_reset_hours", "time_reset_minutes"
1686 }
1687
1688 cols = self.database_link.get_table_column_names(self.tab_pet)
1689 if not isinstance(cols, list):
1690 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1691
1692 # determine which columns from the table we can update based on request body
1693 update_cols = [c for c in cols if c in allowed and c in body]
1694 if not update_cols:
1695 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "fields to update")
1696
1697 sql_data = []
1698 for col in update_cols:
1699 sql_data.append(body[col])
1700
1701 resp = self.database_link.update_data_in_table(
1702 self.tab_pet,
1703 sql_data,
1704 update_cols,
1705 where=f"id={pet_id}"
1706 )
1707 if resp == self.database_link.error:
1708 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1709
1710 bod = self.boilerplate_responses_initialised.build_response_body(
1711 title, "Pet updated successfully", "updated", data.token, error=False
1712 )
1713 return HCI.success(bod)
1714
1715 async def get_pet(self, request: Request) -> Response:
1716 """Get pet information.
1717
1718 Args:
1719 request (Request): The incoming request parameters.
1720 Returns:
1721 Response: The HTTP response to send back to the user.
1722 """
1723 title = "get_pet"
1724 data = self._user_connected(request, title)
1725 if isinstance(data, Response):
1726 return data
1727 body = await self.boilerplate_incoming_initialised.get_body(request)
1728
1729 if "id" not in body:
1730 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "id")
1731
1732 try:
1733 pet_id = int(body["id"])
1734 except (ValueError, TypeError):
1735 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "valid id")
1736
1737 # Check if pet exists and beacon belongs to user
1738 # Query pet once with all needed columns (including beacon for auth)
1739 pet_data = self.database_link.get_data_from_table(
1740 self.tab_pet,
1741 [
1742 "beacon", "name", "food_eaten", "food_max", "food_reset",
1743 "time_reset_hours", "time_reset_minutes"
1744 ],
1745 f"id={pet_id}",
1746 beautify=True
1747 )
1748 if not isinstance(pet_data, list) or len(pet_data) == 0:
1749 return HCI.not_found(
1750 self.boilerplate_responses_initialised.build_response_body(
1751 title,
1752 "Pet not found",
1753 "not_found",
1754 data.token,
1755 error=True
1756 )
1757 )
1758
1759 self.disp.log_debug(
1760 f"Pet data retrieved for pet_id {pet_id}: {pet_data}"
1761 )
1762
1763 # Verify authorization: check if beacon belongs to user
1764 beacon_id = pet_data[0]["beacon"]
1765 beacon_owner = self.database_link.get_data_from_table(
1766 self.tab_beacon,
1767 ["owner"],
1768 f"id={beacon_id}",
1769 beautify=True
1770 )
1771 self.disp.log_debug(
1772 f"Beacon owner for beacon_id {beacon_id}: {beacon_owner}"
1773 )
1774 if not isinstance(beacon_owner, list) or len(beacon_owner) == 0 or beacon_owner[0]["owner"] != data.user_id:
1775 return self.boilerplate_responses_initialised.insuffisant_rights(title, data.token)
1776
1777 # Use the pet data we already retrieved (remove beacon field from response)
1778 pet_dict = {}
1779 for k, v in pet_data[0].items():
1780 if k != "beacon":
1781 pet_dict[k] = v
1782 resp_raw = [pet_dict]
1783
1784 resp = EN_CONST.sanitize_response_data(
1785 resp_raw, disp=self.disp
1786 )
1787
1788 bod = self.boilerplate_responses_initialised.build_response_body(
1789 title, "Pet retrieved successfully", resp, data.token, error=False
1790 )
1791 return HCI.success(bod)
1792
1793 async def delete_pet(self, request: Request) -> Response:
1794 """Delete a pet from the database.
1795
1796 Args:
1797 request (Request): The incoming request parameters.
1798 Returns:
1799 Response: The HTTP response to send back to the user.
1800 """
1801 title = "delete_pet"
1802 data = self._user_connected(request, title)
1803 if isinstance(data, Response):
1804 return data
1805 body = await self.boilerplate_incoming_initialised.get_body(request)
1806 elem = "id"
1807 if elem not in body:
1808 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, elem)
1809
1810 try:
1811 pet_id = int(body["id"])
1812 except (ValueError, TypeError):
1813 return self.boilerplate_responses_initialised.missing_variable_in_body(title, data.token, "valid id")
1814
1815 # Check if pet exists and beacon belongs to user
1816 pet_data = self.database_link.get_data_from_table(
1817 self.tab_pet,
1818 ["beacon"],
1819 f"id={pet_id}",
1820 beautify=True
1821 )
1822 if not isinstance(pet_data, list) or len(pet_data) == 0:
1823 return HCI.not_found(
1824 self.boilerplate_responses_initialised.build_response_body(
1825 title,
1826 "Pet not found",
1827 "not_found",
1828 data.token,
1829 error=True
1830 )
1831 )
1832
1833 beacon_id = pet_data[0]["beacon"]
1834 beacon_owner = self.database_link.get_data_from_table(
1835 self.tab_beacon,
1836 ["owner"],
1837 f"id={beacon_id}",
1838 beautify=True
1839 )
1840 if not isinstance(beacon_owner, list) or len(beacon_owner) == 0 or beacon_owner[0]["owner"] != data.user_id:
1841 return self.boilerplate_responses_initialised.insuffisant_rights(title, data.token)
1842
1843 resp = self.database_link.remove_data_from_table(
1844 self.tab_pet,
1845 f"id={pet_id}"
1846 )
1847 if resp == self.database_link.error:
1848 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1849
1850 bod = self.boilerplate_responses_initialised.build_response_body(
1851 title, "Pet deleted successfully", "deleted", data.token, error=False
1852 )
1853 return HCI.success(bod)
1854
1855 async def get_pets(self, request: Request) -> Response:
1856 """Get all pets for the authenticated user.
1857
1858 Args:
1859 request (Request): The incoming request parameters.
1860
1861 Returns:
1862 Response: The HTTP response with the list of pets.
1863 """
1864 title = "get_pets"
1865 data = self._user_connected(request, title)
1866 if isinstance(data, Response):
1867 return data
1868 beacons_raw = self.database_link.get_data_from_table(
1869 self.tab_beacon,
1870 ["id"],
1871 f"owner={data.user_id}",
1872 beautify=True
1873 )
1874 if not isinstance(beacons_raw, list):
1875 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1876 if len(beacons_raw) == 0:
1877 bod = self.boilerplate_responses_initialised.build_response_body(
1878 title, "The pets have been gathered.", [], data.token, error=False
1879 )
1880 return HCI.success(bod)
1881 beacons = []
1882 for i in enumerate(beacons_raw):
1883 beacons.append(i[1]["id"])
1884 columns = self.database_link.get_table_column_names(self.tab_pet)
1885 if not isinstance(columns, list):
1886 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1887 pets_raw = []
1888 for beacon in beacons:
1889 pet = self.database_link.get_data_from_table(
1890 self.tab_pet,
1891 columns,
1892 f"beacon={beacon}",
1893 beautify=True
1894 )
1895 self.disp.log_debug(f"Pet data for beacon {beacon}: {pet}")
1896 if not isinstance(pet, list):
1897 return self.boilerplate_responses_initialised.internal_server_error(title, data.token)
1898 if len(pet) == 0:
1899 continue
1900 pets_raw.append(pet[0])
1901 self.disp.log_debug(f"Raw pets data: {pets_raw}")
1902 pets = EN_CONST.sanitize_response_data(
1903 pets_raw, disp=self.disp
1904 )
1905 bod = self.boilerplate_responses_initialised.build_response_body(
1906 title, "The pets have been gathered.", pets, data.token, error=False
1907 )
1908 return HCI.success(bod)
None __init__(self, int error=84, int success=0, bool debug=False)
Union[UserInfo, Response] _user_connected(self, Request request, str title="_user_connected")