1111from decimal import Decimal , InvalidOperation
1212import json
1313import sqlite3
14+ from threading import RLock
1415from typing import Any
1516from uuid import uuid4
1617
@@ -46,6 +47,7 @@ class EventLedger:
4647 """SQLite-backed ledger suitable for simulation and a later API adapter."""
4748
4849 def __init__ (self , connection : sqlite3 .Connection ):
50+ self ._lock = RLock ()
4951 database_path = connection .execute ("PRAGMA database_list" ).fetchone ()[2 ]
5052 if database_path :
5153 self ._connection = sqlite3 .connect (database_path , check_same_thread = False )
@@ -128,58 +130,62 @@ def record(
128130 metadata = metadata or {},
129131 created_at = normalized_created_at .isoformat (),
130132 )
131- self ._connection .execute (
132- """
133- INSERT INTO ledger_events
134- (id, category, event_type, team_id, agent_id, task_id, amount, currency,
135- requires_approval, approved_by, metadata_json, created_at)
136- VALUES (:id, :category, :event_type, :team_id, :agent_id, :task_id, :amount,
137- :currency, :requires_approval, :approved_by, :metadata_json, :created_at)
138- """ ,
139- {** asdict (event ), "requires_approval" : int (event .requires_approval ),
140- "amount" : str (event .amount ),
141- "metadata_json" : json .dumps (event .metadata , sort_keys = True )},
142- )
143- self ._connection .commit ()
133+ with self ._lock :
134+ self ._connection .execute (
135+ """
136+ INSERT INTO ledger_events
137+ (id, category, event_type, team_id, agent_id, task_id, amount, currency,
138+ requires_approval, approved_by, metadata_json, created_at)
139+ VALUES (:id, :category, :event_type, :team_id, :agent_id, :task_id, :amount,
140+ :currency, :requires_approval, :approved_by, :metadata_json, :created_at)
141+ """ ,
142+ {** asdict (event ), "requires_approval" : int (event .requires_approval ),
143+ "amount" : str (event .amount ),
144+ "metadata_json" : json .dumps (event .metadata , sort_keys = True )},
145+ )
146+ self ._connection .commit ()
144147 return event
145148
146149 def approve (self , event_id : str , approver_id : str ) -> LedgerEvent :
147150 if not approver_id :
148151 raise ValueError ("approver_id is required" )
149- cursor = self ._connection .execute (
150- """
151- UPDATE ledger_events
152- SET requires_approval = 0, approved_by = ?
153- WHERE id = ? AND requires_approval = 1 AND approved_by IS NULL
154- """ ,
155- (approver_id , event_id ),
156- )
157- if cursor .rowcount != 1 :
158- raise LookupError ("pending approval not found" )
159- self ._connection .commit ()
160- return self .get (event_id )
152+ with self ._lock :
153+ cursor = self ._connection .execute (
154+ """
155+ UPDATE ledger_events
156+ SET requires_approval = 0, approved_by = ?
157+ WHERE id = ? AND requires_approval = 1 AND approved_by IS NULL
158+ """ ,
159+ (approver_id , event_id ),
160+ )
161+ if cursor .rowcount != 1 :
162+ raise LookupError ("pending approval not found" )
163+ self ._connection .commit ()
164+ return self .get (event_id )
161165
162166 def get (self , event_id : str ) -> LedgerEvent :
163- row = self ._connection .execute (
164- "SELECT * FROM ledger_events WHERE id = ?" , (event_id ,)
165- ).fetchone ()
167+ with self ._lock :
168+ row = self ._connection .execute (
169+ "SELECT * FROM ledger_events WHERE id = ?" , (event_id ,)
170+ ).fetchone ()
166171 if row is None :
167172 raise LookupError ("ledger event not found" )
168173 return self ._row_to_event (row )
169174
170175 def list_events (self , team_id : str | None = None , limit : int | None = None ) -> list [LedgerEvent ]:
171176 limit_sql = "" if limit is None else " LIMIT ?"
172177 limit_params : tuple [Any , ...] = () if limit is None else (limit ,)
173- if team_id is None :
174- rows = self ._connection .execute (
175- "SELECT * FROM ledger_events ORDER BY created_at DESC" + limit_sql ,
176- limit_params ,
177- ).fetchall ()
178- else :
179- rows = self ._connection .execute (
180- "SELECT * FROM ledger_events WHERE team_id = ? ORDER BY created_at DESC" + limit_sql ,
181- (team_id , * limit_params ),
182- ).fetchall ()
178+ with self ._lock :
179+ if team_id is None :
180+ rows = self ._connection .execute (
181+ "SELECT * FROM ledger_events ORDER BY created_at DESC" + limit_sql ,
182+ limit_params ,
183+ ).fetchall ()
184+ else :
185+ rows = self ._connection .execute (
186+ "SELECT * FROM ledger_events WHERE team_id = ? ORDER BY created_at DESC" + limit_sql ,
187+ (team_id , * limit_params ),
188+ ).fetchall ()
183189 return [self ._row_to_event (row ) for row in rows ]
184190
185191 def list_pending_approvals (
@@ -194,21 +200,23 @@ def list_pending_approvals(
194200 if limit is not None :
195201 query += " LIMIT ?"
196202 params += (limit ,)
197- rows = self ._connection .execute (query , params ).fetchall ()
203+ with self ._lock :
204+ rows = self ._connection .execute (query , params ).fetchall ()
198205 return [self ._row_to_event (row ) for row in rows ]
199206
200207 def summarize (self , team_id : str | None = None ) -> list [TeamSummary ]:
201208 where = "" if team_id is None else "WHERE team_id = ?"
202209 params : tuple [Any , ...] = () if team_id is None else (team_id ,)
203- rows = self ._connection .execute (
204- """
205- SELECT team_id, currency, category, requires_approval, amount
206- FROM ledger_events
207- """ + where + """
208- ORDER BY team_id, currency
209- """ ,
210- params ,
211- ).fetchall ()
210+ with self ._lock :
211+ rows = self ._connection .execute (
212+ """
213+ SELECT team_id, currency, category, requires_approval, amount
214+ FROM ledger_events
215+ """ + where + """
216+ ORDER BY team_id, currency
217+ """ ,
218+ params ,
219+ ).fetchall ()
212220 totals : dict [tuple [str , str ], dict [str , Any ]] = {}
213221 for row in rows :
214222 key = (row ["team_id" ], row ["currency" ])
0 commit comments