samir12321 commited on
Commit
20c24c0
·
verified ·
1 Parent(s): 4e02800

Upload signals_db.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. signals_db.py +67 -12
signals_db.py CHANGED
@@ -4,26 +4,43 @@ Stores one row per inference call and records the actual buyer outcome
4
  when it becomes available — used as training labels on the next retrain.
5
  """
6
 
 
7
  import sqlite3
 
8
  from pathlib import Path
9
  from typing import Optional
10
  import pandas as pd
11
 
12
- DB_PATH = Path(__file__).parent / "signals.db"
 
 
13
 
14
 
15
  def get_connection(db_path: Path = DB_PATH) -> sqlite3.Connection:
16
- """Return a WAL-mode connection with row_factory = sqlite3.Row."""
17
- conn = sqlite3.connect(str(db_path))
 
 
 
 
 
 
 
 
 
 
18
  conn.row_factory = sqlite3.Row
19
  conn.execute("PRAGMA journal_mode=WAL")
20
  conn.execute("PRAGMA foreign_keys=ON")
 
21
  return conn
22
 
23
 
24
  def init_db(db_path: Path = DB_PATH) -> None:
25
  """Create the prediction_log table and indexes. Idempotent."""
26
- with get_connection(db_path) as conn:
 
 
27
  conn.executescript("""
28
  CREATE TABLE IF NOT EXISTS prediction_log (
29
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -53,7 +70,7 @@ def log_prediction(
53
  db_path: Path = DB_PATH,
54
  ) -> None:
55
  """Insert a new prediction entry. One row per prediction call."""
56
- with get_connection(db_path) as conn:
57
  conn.execute(
58
  """INSERT INTO prediction_log
59
  (record_id, predicted_at, predicted_probability, predicted_label, predicted_roi_pct)
@@ -68,27 +85,65 @@ def record_outcome(
68
  outcome_at: str,
69
  db_path: Path = DB_PATH,
70
  ) -> int:
71
- """Update the latest unresolved prediction for record_id with the actual outcome.
72
- Returns number of rows updated."""
73
- with get_connection(db_path) as conn:
 
 
 
 
74
  cursor = conn.execute(
75
  """UPDATE prediction_log
76
  SET actual_outcome = ?, outcome_at = ?
77
- WHERE record_id = ? AND actual_outcome IS NULL""",
 
 
 
 
 
78
  (actual_outcome, outcome_at, record_id),
79
  )
80
  return cursor.rowcount
81
 
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def get_prediction_log_df(db_path: Path = DB_PATH) -> pd.DataFrame:
84
  """Return the full prediction log as a DataFrame."""
85
- with get_connection(db_path) as conn:
86
  return pd.read_sql_query("SELECT * FROM prediction_log ORDER BY predicted_at DESC", conn)
87
 
88
 
89
  def get_resolved_predictions(db_path: Path = DB_PATH) -> pd.DataFrame:
90
  """Return only predictions where the actual outcome is known."""
91
- with get_connection(db_path) as conn:
92
  return pd.read_sql_query(
93
  "SELECT * FROM prediction_log WHERE actual_outcome IS NOT NULL ORDER BY outcome_at DESC",
94
  conn,
@@ -97,7 +152,7 @@ def get_resolved_predictions(db_path: Path = DB_PATH) -> pd.DataFrame:
97
 
98
  def db_summary(db_path: Path = DB_PATH) -> dict:
99
  """Return row count for prediction_log."""
100
- with get_connection(db_path) as conn:
101
  count = conn.execute("SELECT COUNT(*) FROM prediction_log").fetchone()[0]
102
  return {"prediction_log": count}
103
 
 
4
  when it becomes available — used as training labels on the next retrain.
5
  """
6
 
7
+ import os
8
  import sqlite3
9
+ from contextlib import closing
10
  from pathlib import Path
11
  from typing import Optional
12
  import pandas as pd
13
 
14
+ # Prefer a persistent-volume mount so the feedback log survives HF Space restarts.
15
+ # Override with SIGNALS_DB_PATH env var for local dev or alternative mount points.
16
+ DB_PATH = Path(os.environ.get("SIGNALS_DB_PATH", "/data/signals.db"))
17
 
18
 
19
  def get_connection(db_path: Path = DB_PATH) -> sqlite3.Connection:
20
+ """Return a WAL-mode connection.
21
+
22
+ check_same_thread=False: FastAPI runs sync handlers in a thread pool and
23
+ a daemon thread drives retraining — connections must be usable from any
24
+ thread that opens them. Each call site opens *and closes* its own
25
+ connection, so there is no shared-connection cross-thread hazard.
26
+
27
+ busy_timeout=5000: WAL mode handles read/write concurrency well, but a
28
+ brief write-lock collision (e.g. retrain thread + request thread writing
29
+ simultaneously) would otherwise raise OperationalError immediately.
30
+ """
31
+ conn = sqlite3.connect(str(db_path), check_same_thread=False)
32
  conn.row_factory = sqlite3.Row
33
  conn.execute("PRAGMA journal_mode=WAL")
34
  conn.execute("PRAGMA foreign_keys=ON")
35
+ conn.execute("PRAGMA busy_timeout=5000")
36
  return conn
37
 
38
 
39
  def init_db(db_path: Path = DB_PATH) -> None:
40
  """Create the prediction_log table and indexes. Idempotent."""
41
+ db_path = Path(db_path)
42
+ db_path.parent.mkdir(parents=True, exist_ok=True)
43
+ with closing(get_connection(db_path)) as conn, conn:
44
  conn.executescript("""
45
  CREATE TABLE IF NOT EXISTS prediction_log (
46
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
70
  db_path: Path = DB_PATH,
71
  ) -> None:
72
  """Insert a new prediction entry. One row per prediction call."""
73
+ with closing(get_connection(db_path)) as conn, conn:
74
  conn.execute(
75
  """INSERT INTO prediction_log
76
  (record_id, predicted_at, predicted_probability, predicted_label, predicted_roi_pct)
 
85
  outcome_at: str,
86
  db_path: Path = DB_PATH,
87
  ) -> int:
88
+ """Update the single most-recent unresolved prediction for record_id.
89
+
90
+ Returns 1 if a row was updated, 0 if no unresolved prediction existed.
91
+ Targets only the latest row so that a record scored multiple times before
92
+ the buyer responds does not get every prior prediction overwritten.
93
+ """
94
+ with closing(get_connection(db_path)) as conn, conn:
95
  cursor = conn.execute(
96
  """UPDATE prediction_log
97
  SET actual_outcome = ?, outcome_at = ?
98
+ WHERE id = (
99
+ SELECT id FROM prediction_log
100
+ WHERE record_id = ? AND actual_outcome IS NULL
101
+ ORDER BY predicted_at DESC
102
+ LIMIT 1
103
+ )""",
104
  (actual_outcome, outcome_at, record_id),
105
  )
106
  return cursor.rowcount
107
 
108
 
109
+ def record_outcomes_many(
110
+ outcomes: list[dict],
111
+ db_path: Path = DB_PATH,
112
+ ) -> int:
113
+ """Update the most-recent unresolved prediction for each record_id in one transaction.
114
+
115
+ outcomes: list of dicts with keys record_id, actual_outcome, outcome_at.
116
+ Returns total rows updated.
117
+ """
118
+ if not outcomes:
119
+ return 0
120
+ with closing(get_connection(db_path)) as conn, conn:
121
+ total = 0
122
+ for o in outcomes:
123
+ cursor = conn.execute(
124
+ """UPDATE prediction_log
125
+ SET actual_outcome = ?, outcome_at = ?
126
+ WHERE id = (
127
+ SELECT id FROM prediction_log
128
+ WHERE record_id = ? AND actual_outcome IS NULL
129
+ ORDER BY predicted_at DESC
130
+ LIMIT 1
131
+ )""",
132
+ (o["actual_outcome"], o["outcome_at"], o["record_id"]),
133
+ )
134
+ total += cursor.rowcount
135
+ return total
136
+
137
+
138
  def get_prediction_log_df(db_path: Path = DB_PATH) -> pd.DataFrame:
139
  """Return the full prediction log as a DataFrame."""
140
+ with closing(get_connection(db_path)) as conn:
141
  return pd.read_sql_query("SELECT * FROM prediction_log ORDER BY predicted_at DESC", conn)
142
 
143
 
144
  def get_resolved_predictions(db_path: Path = DB_PATH) -> pd.DataFrame:
145
  """Return only predictions where the actual outcome is known."""
146
+ with closing(get_connection(db_path)) as conn:
147
  return pd.read_sql_query(
148
  "SELECT * FROM prediction_log WHERE actual_outcome IS NOT NULL ORDER BY outcome_at DESC",
149
  conn,
 
152
 
153
  def db_summary(db_path: Path = DB_PATH) -> dict:
154
  """Return row count for prediction_log."""
155
+ with closing(get_connection(db_path)) as conn:
156
  count = conn.execute("SELECT COUNT(*) FROM prediction_log").fetchone()[0]
157
  return {"prediction_log": count}
158