TKK_E32232028/db.py

425 lines
14 KiB
Python

import os
import mysql.connector
from mysql.connector import errorcode
from supabase import create_client, Client
from dotenv import load_dotenv
# Load environment variables from .env file using absolute path relative to this script
current_dir = os.path.dirname(os.path.abspath(__file__))
dotenv_path = os.path.join(current_dir, ".env")
load_dotenv(dotenv_path=dotenv_path)
# ==============================================================================
# Schema (unified single table):
#
# CREATE TABLE IF NOT EXISTS data_fcgntion (
# id_tabel INT NOT NULL AUTO_INCREMENT,
# create_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
# person VARCHAR(128) NULL,
# rfid_uid VARCHAR(64) NULL DEFAULT NULL,
# fc JSON NULL DEFAULT NULL,
# PRIMARY KEY (id_tabel),
# UNIQUE KEY uniq_rfid (rfid_uid),
# KEY idx_person (person)
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#
# CREATE TABLE IF NOT EXISTS attendance_log (
# id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
# uid VARCHAR(32) NOT NULL,
# recognized_name VARCHAR(128) NULL,
# face_ok TINYINT(1) NOT NULL,
# created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
# PRIMARY KEY (id),
# KEY idx_uid_time (uid, created_at)
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# ==============================================================================
def get_connection_from_env():
"""Connect to MySQL from .env config. Auto-creates the database if missing."""
cfg = {
"user": os.environ.get("DB_USER", "root"),
"password": os.environ.get("DB_PASSWORD", ""),
"host": os.environ.get("DB_HOST", "127.0.0.1"),
"port": int(os.environ.get("DB_PORT", 3306)),
"database": os.environ.get("DB_NAME", "face_db"),
}
try:
conn = mysql.connector.connect(**cfg)
except mysql.connector.Error as err:
if err.errno == errorcode.ER_BAD_DB_ERROR:
tmp_cfg = cfg.copy()
tmp_cfg.pop("database")
conn = mysql.connector.connect(**tmp_cfg)
cursor = conn.cursor()
cursor.execute(f"CREATE DATABASE {cfg['database']}")
conn.database = cfg["database"]
else:
raise
return conn
def get_supabase_client() -> "Client | None":
"""Get Supabase client from environment variables."""
url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_ANON_KEY")
if not url or not key:
return None
try:
return create_client(url, key)
except Exception as e:
print(f"[SUPABASE] Error creating client: {e}")
return None
# ==============================================================================
# TABLE SETUP
# ==============================================================================
def ensure_data_table(conn):
"""Ensure unified data_fcgntion table and attendance_log table exist."""
cursor = conn.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS data_fcgntion (
id_tabel INT NOT NULL AUTO_INCREMENT,
create_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
person VARCHAR(128) NULL,
rfid_uid VARCHAR(64) NULL DEFAULT NULL,
fc JSON NULL DEFAULT NULL,
PRIMARY KEY (id_tabel),
UNIQUE KEY uniq_rfid (rfid_uid),
KEY idx_person (person)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS attendance_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
uid VARCHAR(32) NOT NULL,
recognized_name VARCHAR(128) NULL,
face_ok TINYINT(1) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_uid_time (uid, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
)
conn.commit()
# ==============================================================================
# MODE DAFTAR - Enrollment Functions
# ==============================================================================
def get_or_create_data_row(conn, name: str) -> int:
"""Get or create a row in data_fcgntion by person name. Returns id_tabel."""
name = (name or "").strip()
if not name:
raise ValueError("name is empty")
cursor = conn.cursor()
cursor.execute("SELECT id_tabel FROM data_fcgntion WHERE person=%s LIMIT 1", (name,))
row = cursor.fetchone()
if row:
return int(row[0])
cursor.execute("INSERT INTO data_fcgntion (person) VALUES (%s)", (name,))
conn.commit()
return int(cursor.lastrowid)
def save_or_update_person(conn, name: str, embedding_json_str: str,
rfid_uid=None) -> int:
"""Save face embedding for a person. Updates existing row or inserts new.
Returns id_tabel.
"""
name = (name or "").strip()
cursor = conn.cursor()
cursor.execute("SELECT id_tabel FROM data_fcgntion WHERE person=%s LIMIT 1", (name,))
row = cursor.fetchone()
if row:
row_id = int(row[0])
if rfid_uid:
cursor.execute(
"UPDATE data_fcgntion SET fc=%s, rfid_uid=COALESCE(%s, rfid_uid) WHERE id_tabel=%s",
(embedding_json_str, rfid_uid, row_id),
)
else:
cursor.execute(
"UPDATE data_fcgntion SET fc=%s WHERE id_tabel=%s",
(embedding_json_str, row_id),
)
conn.commit()
return row_id
else:
cursor.execute(
"INSERT INTO data_fcgntion (person, rfid_uid, fc) VALUES (%s, %s, %s)",
(name, rfid_uid, embedding_json_str),
)
conn.commit()
return int(cursor.lastrowid)
def upsert_person_rfid(conn, rfid_uid: str, name=None):
"""Insert or update rfid_uid in data_fcgntion.
Priority:
1. rfid_uid already in table -> update name if given, done.
2. row with matching name exists and no rfid -> assign rfid_uid.
3. Otherwise insert new row.
"""
rfid_uid = (rfid_uid or "").strip().upper()
if not rfid_uid:
return
cursor = conn.cursor()
cursor.execute("SELECT id_tabel FROM data_fcgntion WHERE rfid_uid=%s LIMIT 1", (rfid_uid,))
existing = cursor.fetchone()
if existing:
if name:
cursor.execute(
"UPDATE data_fcgntion SET person=COALESCE(%s, person) WHERE id_tabel=%s",
(name, int(existing[0])),
)
conn.commit()
return
if name:
cursor.execute(
"SELECT id_tabel FROM data_fcgntion WHERE person=%s AND rfid_uid IS NULL LIMIT 1",
(name,),
)
row = cursor.fetchone()
if row:
cursor.execute(
"UPDATE data_fcgntion SET rfid_uid=%s WHERE id_tabel=%s",
(rfid_uid, int(row[0])),
)
conn.commit()
return
cursor.execute(
"INSERT INTO data_fcgntion (person, rfid_uid) VALUES (%s, %s)",
(name or rfid_uid, rfid_uid),
)
conn.commit()
def claim_rfid_by_name(conn, name: str, rfid_uid: str) -> bool:
"""Assign rfid_uid to a row where person=name and rfid_uid IS NULL.
Returns True if a row was updated.
"""
cursor = conn.cursor()
cursor.execute(
"SELECT id_tabel FROM data_fcgntion WHERE person=%s AND rfid_uid IS NULL LIMIT 1",
(name,),
)
row = cursor.fetchone()
if not row:
return False
cursor.execute(
"UPDATE data_fcgntion SET rfid_uid=%s WHERE id_tabel=%s",
(rfid_uid.strip().upper(), int(row[0])),
)
conn.commit()
return True
# ==============================================================================
# MODE ABSEN - Recognition / Attendance Functions
# ==============================================================================
def load_all_faces(conn) -> list:
"""Load all face embeddings from data_fcgntion where fc IS NOT NULL.
Returns list of dicts: {id, name, rfid_uid, embedding}.
"""
cursor = conn.cursor()
cursor.execute(
"SELECT id_tabel, person, rfid_uid, fc FROM data_fcgntion WHERE fc IS NOT NULL"
)
rows = cursor.fetchall()
result = []
for r in rows:
result.append({"id": r[0], "name": r[1], "rfid_uid": r[2], "embedding": r[3]})
return result
def get_person_by_rfid(conn, rfid_uid):
"""Get data_fcgntion row by rfid_uid. Returns dict or None."""
if rfid_uid is None:
return None
rfid_uid = rfid_uid.strip().upper()
cursor = conn.cursor()
cursor.execute(
"SELECT id_tabel, person, rfid_uid, fc, create_at FROM data_fcgntion WHERE rfid_uid=%s LIMIT 1",
(rfid_uid,),
)
row = cursor.fetchone()
if not row:
return None
return {
"id": row[0],
"person": row[1],
"rfid_uid": row[2],
"embedding": row[3], # <-- fc field (JSON embedding)
"has_face": row[3] is not None,
"create_at": row[4],
}
def get_person_by_name(conn, name: str):
"""Get data_fcgntion row by person name. Returns dict or None."""
cursor = conn.cursor()
cursor.execute(
"SELECT id_tabel, person, rfid_uid, fc, create_at FROM data_fcgntion WHERE person=%s LIMIT 1",
(name.strip(),),
)
row = cursor.fetchone()
if not row:
return None
return {
"id": row[0],
"person": row[1],
"rfid_uid": row[2],
"has_face": row[3] is not None,
"create_at": row[4],
}
def log_attendance(conn, uid: str, recognized_name, face_ok: bool):
"""Log an attendance event and update create_at on matching data_fcgntion row."""
cursor = conn.cursor()
cursor.execute(
"INSERT INTO attendance_log (uid, recognized_name, face_ok) VALUES (%s, %s, %s)",
(uid, recognized_name, 1 if face_ok else 0),
)
cursor.execute(
"UPDATE data_fcgntion SET create_at=CURRENT_TIMESTAMP WHERE UPPER(TRIM(rfid_uid))=UPPER(TRIM(%s))",
(uid,),
)
rows_affected = cursor.rowcount
conn.commit()
print(f"[ATTENDANCE] uid={uid} name={recognized_name} face_ok={face_ok} rows={rows_affected}")
if rows_affected == 0:
print(f"[ATTENDANCE] WARNING: no data_fcgntion row matched rfid_uid={uid}")
if recognized_name and recognized_name != "Unknown":
send_attendance_to_supabase(recognized_name)
def send_attendance_to_supabase(recognized_name: str):
"""Send attendance record to Supabase output_alat table (non-blocking background thread)."""
import threading
import requests
def _push():
try:
url = os.environ.get("SUPABASE_URL", "").rstrip("/")
key = os.environ.get("SUPABASE_ANON_KEY", "")
if not url or not key:
return
headers = {
"apikey": key,
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"Prefer": "return=minimal",
}
resp = requests.post(f"{url}/rest/v1/output_alat", headers=headers,
json={"nama_user": recognized_name}, timeout=10)
if resp.status_code in (200, 201):
print(f"[SUPABASE] sent nama_user='{recognized_name}' status={resp.status_code}")
else:
print(f"[SUPABASE] failed status={resp.status_code} body={resp.text[:200]}")
except Exception as e:
print(f"[SUPABASE] exception: {e}")
threading.Thread(target=_push, daemon=True).start()
# ==============================================================================
# LIST / READ
# ==============================================================================
def list_all_persons(conn, limit: int = 200) -> list:
"""List all rows from data_fcgntion ordered newest first."""
cursor = conn.cursor()
cursor.execute(
"SELECT id_tabel, person, rfid_uid, create_at, "
"CASE WHEN fc IS NOT NULL THEN 1 ELSE 0 END AS has_face "
"FROM data_fcgntion ORDER BY create_at DESC LIMIT %s",
(int(limit),),
)
rows = cursor.fetchall()
result = []
for r in rows:
result.append({
"id": r[0],
"person": r[1],
"rfid_uid": r[2],
"create_at": r[3],
"has_face": bool(r[4]),
})
return result
def list_attendance_log(conn, limit: int = 200) -> list:
"""List attendance log entries ordered newest first."""
cursor = conn.cursor()
cursor.execute(
"SELECT uid, recognized_name, face_ok, created_at "
"FROM attendance_log ORDER BY created_at DESC LIMIT %s",
(int(limit),),
)
rows = cursor.fetchall()
result = []
for r in rows:
result.append({
"uid": r[0],
"recognized_name": r[1],
"face_ok": bool(r[2]),
"created_at": r[3],
})
return result
# ==============================================================================
# MODE HAPUS - Delete / Cleanup
# ==============================================================================
def delete_person_by_rfid(conn, rfid_uid: str):
"""Delete data_fcgntion row by rfid_uid."""
cursor = conn.cursor()
cursor.execute("DELETE FROM data_fcgntion WHERE rfid_uid=%s", (rfid_uid.strip().upper(),))
conn.commit()
def delete_person_by_name(conn, name: str):
"""Delete data_fcgntion row(s) by person name."""
cursor = conn.cursor()
cursor.execute("DELETE FROM data_fcgntion WHERE person=%s", (name.strip(),))
conn.commit()
def delete_unused_persons(conn) -> int:
"""Delete rows where BOTH fc IS NULL AND rfid_uid IS NULL.
Returns number of rows deleted.
"""
cursor = conn.cursor()
cursor.execute("DELETE FROM data_fcgntion WHERE fc IS NULL AND rfid_uid IS NULL")
count = cursor.rowcount
conn.commit()
print(f"[CLEANUP] Deleted {count} unused rows (fc=NULL, rfid=NULL).")
return count
def delete_old_attendance_logs(conn, days: int = 30) -> int:
"""Delete attendance_log rows older than `days` days.
Returns number of rows deleted.
"""
cursor = conn.cursor()
cursor.execute(
"DELETE FROM attendance_log WHERE created_at < NOW() - INTERVAL %s DAY",
(int(days),),
)
count = cursor.rowcount
conn.commit()
print(f"[CLEANUP] Deleted {count} attendance log rows older than {days} days.")
return count