340 lines
12 KiB
Python
340 lines
12 KiB
Python
import sqlite3
|
|
import json
|
|
import os
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
# Path to the SQLite database file
|
|
DB_PATH = os.path.join(os.path.dirname(__file__), "/db_file/fitness.db")
|
|
|
|
# Ensure the database directory exists
|
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper functions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _get_connection() -> sqlite3.Connection:
|
|
"""Return a new SQLite connection with row factory set to sqlite3.Row."""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Database initialization
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def initialize_db() -> None:
|
|
"""Create the database file and tables if they do not already exist.
|
|
|
|
This function is idempotent - calling it multiple times will not alter an
|
|
already-existing schema.
|
|
"""
|
|
with _get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
username TEXT UNIQUE NOT NULL,
|
|
age INTEGER,
|
|
photo TEXT,
|
|
settings TEXT
|
|
);
|
|
"""
|
|
)
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS providers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
phone TEXT,
|
|
age INTEGER,
|
|
photo TEXT,
|
|
rates TEXT,
|
|
private INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
"""
|
|
)
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS visits (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_ids TEXT NOT NULL,
|
|
provider_ids TEXT NOT NULL,
|
|
cost REAL,
|
|
location TEXT,
|
|
timestamp DATETIME NOT NULL
|
|
);
|
|
"""
|
|
)
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
name TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
# Initialize default settings
|
|
# Use JSON string for array values
|
|
cursor.execute(
|
|
"INSERT OR REPLACE INTO settings (name, value) VALUES (?, ?)",
|
|
("rate_options", "half_hour,hour"),
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CRUD functions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _select(table: str, where_clause: str = "", params: tuple = (), fields: List[str] = None) -> List[Dict[str, Any]]:
|
|
"""Internal helper to execute SELECT queries.
|
|
|
|
Parameters
|
|
----------
|
|
table: str
|
|
Name of the table to query.
|
|
where_clause: str
|
|
Optional WHERE clause (without the leading "WHERE").
|
|
params: tuple
|
|
Parameters for the query.
|
|
fields: List[str] | None
|
|
List of columns to retrieve. If None, all columns are returned.
|
|
"""
|
|
cols = ", ".join(fields) if fields else "*"
|
|
sql = f"SELECT {cols} FROM {table}"
|
|
if where_clause:
|
|
sql += f" WHERE {where_clause}"
|
|
with _get_connection() as conn:
|
|
cursor = conn.execute(sql, params)
|
|
rows = [dict(row) for row in cursor.fetchall()]
|
|
return rows
|
|
|
|
# Users ---------------------------------------------------------------------
|
|
|
|
def get_user(name: Optional[str] = None, id: Optional[int] = None, username: Optional[str] = None, fields: List[str] = None) -> List[Dict[str, Any]]:
|
|
where_clauses = []
|
|
params = []
|
|
if name:
|
|
where_clauses.append("name = ?")
|
|
params.append(name)
|
|
if id is not None:
|
|
where_clauses.append("id = ?")
|
|
params.append(id)
|
|
if username:
|
|
where_clauses.append("username = ?")
|
|
params.append(username)
|
|
where = " AND ".join(where_clauses) if where_clauses else ""
|
|
return _select("users", where, tuple(params), fields)
|
|
|
|
|
|
def add_user(name: str, age: int, photo: str, username: str) -> Dict[str, Any]:
|
|
"""Insert a new user.
|
|
|
|
The ``settings`` column is initialised to an empty JSON object.
|
|
"""
|
|
with _get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"INSERT INTO users (name, username, age, photo, settings) VALUES (?, ?, ?, ?, ?)",
|
|
(name, username, age, photo, json.dumps({})),
|
|
)
|
|
conn.commit()
|
|
return {"status": "ok", "id": cursor.lastrowid}
|
|
|
|
|
|
def update_user(id: int, name: Optional[str] = None, photo: Optional[str] = None, settings: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
updates = []
|
|
params = []
|
|
if name is not None:
|
|
updates.append("name = ?")
|
|
params.append(name)
|
|
if photo is not None:
|
|
updates.append("photo = ?")
|
|
params.append(photo)
|
|
if settings is not None:
|
|
updates.append("settings = ?")
|
|
params.append(json.dumps(settings))
|
|
if not updates:
|
|
return {"status": "no-op"}
|
|
params.append(id)
|
|
with _get_connection() as conn:
|
|
conn.execute(f"UPDATE users SET {', '.join(updates)} WHERE id = ?", tuple(params))
|
|
conn.commit()
|
|
return {"status": "ok", "id": id}
|
|
|
|
# Providers -----------------------------------------------------------------
|
|
|
|
def get_provider(name: Optional[str] = None, id: Optional[int] = None, phone: Optional[str] = None, fields: List[str] = None) -> List[Dict[str, Any]]:
|
|
where_clauses = []
|
|
params = []
|
|
if name:
|
|
where_clauses.append("name = ?")
|
|
params.append(name)
|
|
if id is not None:
|
|
where_clauses.append("id = ?")
|
|
params.append(id)
|
|
if phone:
|
|
where_clauses.append("phone = ?")
|
|
params.append(phone)
|
|
where = " AND ".join(where_clauses) if where_clauses else ""
|
|
return _select("providers", where, tuple(params), fields)
|
|
|
|
|
|
def add_provider(name: str, age: int, photo: str, phone: str, rates: Dict[str, Any], private: bool) -> Dict[str, Any]:
|
|
with _get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"INSERT INTO providers (name, phone, age, photo, rates, private) VALUES (?, ?, ?, ?, ?, ?)",
|
|
(name, phone, age, photo, json.dumps(rates), int(private)),
|
|
)
|
|
conn.commit()
|
|
return {"status": "ok", "id": cursor.lastrowid}
|
|
|
|
|
|
def update_provider(id: int, name: Optional[str] = None, phone: Optional[str] = None, rates: Optional[Dict[str, Any]] = None, private: Optional[bool] = None, photo: Optional[str] = None) -> Dict[str, Any]:
|
|
updates = []
|
|
params = []
|
|
if name is not None:
|
|
updates.append("name = ?")
|
|
params.append(name)
|
|
if phone is not None:
|
|
updates.append("phone = ?")
|
|
params.append(phone)
|
|
if rates is not None:
|
|
updates.append("rates = ?")
|
|
params.append(json.dumps(rates))
|
|
if private is not None:
|
|
updates.append("private = ?")
|
|
params.append(int(private))
|
|
if photo is not None:
|
|
updates.append("photo = ?")
|
|
params.append(photo)
|
|
if not updates:
|
|
return {"status": "no-op"}
|
|
params.append(id)
|
|
with _get_connection() as conn:
|
|
conn.execute(f"UPDATE providers SET {', '.join(updates)} WHERE id = ?", tuple(params))
|
|
conn.commit()
|
|
return {"status": "ok", "id": id}
|
|
|
|
# Visits ---------------------------------------------------------------------
|
|
|
|
def _parse_user_provider_ids(ids_json: str) -> List[int]:
|
|
try:
|
|
return json.loads(ids_json)
|
|
except json.JSONDecodeError:
|
|
return []
|
|
|
|
|
|
def get_visits(provider: Optional[int] = None, user: Optional[int] = None, date_range: Optional[List[str]] = None) -> List[Dict[str, Any]]:
|
|
"""Return visits filtered by optional provider, user, and date range.
|
|
|
|
SQLite does not have a native ``IN`` operator that works directly on a JSON
|
|
array column. We use ``EXISTS`` with ``json_each`` to test whether a value
|
|
appears in the stored JSON array. This keeps the query simple and works
|
|
across SQLite versions.
|
|
"""
|
|
where_clauses: List[str] = []
|
|
params: List[Any] = []
|
|
if provider is not None:
|
|
where_clauses.append("EXISTS (SELECT 1 FROM json_each(provider_ids) WHERE value = ?)")
|
|
params.append(provider)
|
|
if user is not None:
|
|
where_clauses.append("EXISTS (SELECT 1 FROM json_each(user_ids) WHERE value = ?)")
|
|
params.append(user)
|
|
if date_range:
|
|
where_clauses.append("timestamp BETWEEN ? AND ?")
|
|
params.extend(date_range)
|
|
where = " AND ".join(where_clauses) if where_clauses else ""
|
|
rows = _select("visits", where, tuple(params))
|
|
# Convert JSON id fields back to lists
|
|
for row in rows:
|
|
row["user_ids"] = _parse_user_provider_ids(row["user_ids"])
|
|
row["provider_ids"] = _parse_user_provider_ids(row["provider_ids"])
|
|
return rows
|
|
|
|
|
|
def add_visit(user_ids: List[int], provider_ids: List[int], cost: float, location: str, time: str) -> Dict[str, Any]:
|
|
with _get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"INSERT INTO visits (user_ids, provider_ids, cost, location, timestamp) VALUES (?, ?, ?, ?, ?)",
|
|
(json.dumps(user_ids), json.dumps(provider_ids), cost, location, time),
|
|
)
|
|
conn.commit()
|
|
return {"status": "ok", "id": cursor.lastrowid}
|
|
|
|
|
|
def update_visit(id: int, user_ids: Optional[List[int]] = None, provider_ids: Optional[List[int]] = None, cost: Optional[float] = None, location: Optional[str] = None, time: Optional[str] = None) -> Dict[str, Any]:
|
|
updates = []
|
|
params = []
|
|
if user_ids is not None:
|
|
updates.append("user_ids = ?")
|
|
params.append(json.dumps(user_ids))
|
|
if provider_ids is not None:
|
|
updates.append("provider_ids = ?")
|
|
params.append(json.dumps(provider_ids))
|
|
if cost is not None:
|
|
updates.append("cost = ?")
|
|
params.append(cost)
|
|
if location is not None:
|
|
updates.append("location = ?")
|
|
params.append(location)
|
|
if time is not None:
|
|
updates.append("timestamp = ?")
|
|
params.append(time)
|
|
if not updates:
|
|
return {"status": "no-op"}
|
|
params.append(id)
|
|
with _get_connection() as conn:
|
|
conn.execute(f"UPDATE visits SET {', '.join(updates)} WHERE id = ?", tuple(params))
|
|
conn.commit()
|
|
return {"status": "ok", "id": id}
|
|
|
|
# --- Settings helper functions ---
|
|
|
|
def add_setting(name: str, value: str) -> Dict[str, Any]:
|
|
"""Insert a new setting. If a setting with the same name exists, it will be replaced.
|
|
Returns a status dictionary.
|
|
"""
|
|
with _get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"INSERT OR REPLACE INTO settings (name, value) VALUES (?, ?)",
|
|
(name, value),
|
|
)
|
|
conn.commit()
|
|
return {"status": "ok", "name": name}
|
|
|
|
|
|
def change_setting(name: str, value: str) -> Dict[str, Any]:
|
|
"""Update the value of an existing setting. If the setting does not exist, return not-found.
|
|
"""
|
|
with _get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"UPDATE settings SET value = ? WHERE name = ?",
|
|
(value, name),
|
|
)
|
|
conn.commit()
|
|
if cursor.rowcount == 0:
|
|
return {"status": "not-found", "name": name}
|
|
return {"status": "ok", "name": name}
|
|
|
|
|
|
def view_settings(names: List[str] | None = None) -> List[Dict[str, Any]]:
|
|
"""Return a list of settings.
|
|
If names is provided, only those names are returned.
|
|
"""
|
|
if not names:
|
|
return _select("settings")
|
|
placeholders = ", ".join(["?" for _ in names])
|
|
rows = _select("settings", f"name IN ({placeholders})", tuple(names))
|
|
return rows
|
|
|
|
# Example usage (to be removed or guarded by ``if __name__ == '__main__'``)
|
|
if __name__ == "__main__":
|
|
initialize_db()
|
|
# Demo: add a user
|
|
res = add_user("Alice", 30, "alice.jpg", "alice123")
|
|
print("Added user:", res)
|