"""Small, thread-safe shared-recognition quota; stores timestamps, never keys.""" from contextlib import contextmanager from pathlib import Path import sqlite3 import threading import time class SharedLimit(Exception): pass class SharedUsage: def __init__(self,path,daily=200,per_minute=20,concurrent=2): self.path=Path(path);self.daily=daily;self.per_minute=per_minute self.slots=threading.BoundedSemaphore(concurrent) self.path.parent.mkdir(parents=True,exist_ok=True) with sqlite3.connect(self.path) as db: db.execute('CREATE TABLE IF NOT EXISTS recognition_usage (started REAL NOT NULL)') db.execute('CREATE INDEX IF NOT EXISTS recognition_started ON recognition_usage(started)') def summary(self,now=None): now=time.time() if now is None else now with sqlite3.connect(self.path) as db: used=db.execute('SELECT COUNT(*) FROM recognition_usage WHERE started>?',(now-86400,)).fetchone()[0] return {'per_24_hours':self.daily,'per_minute':self.per_minute,'used_24_hours':used,'remaining':max(0,self.daily-used)} def reserve(self,now=None): now=time.time() if now is None else now with sqlite3.connect(self.path,timeout=10,isolation_level=None) as db: db.execute('BEGIN IMMEDIATE') try: db.execute('DELETE FROM recognition_usage WHERE started<=?',(now-86400,)) daily=db.execute('SELECT COUNT(*) FROM recognition_usage').fetchone()[0] minute=db.execute('SELECT COUNT(*) FROM recognition_usage WHERE started>?',(now-60,)).fetchone()[0] if daily>=self.daily:raise SharedLimit('The classroom’s shared recognition allowance has been used for now. Please try again later or ask the teacher.') if minute>=self.per_minute:raise SharedLimit('The classroom is making lots of guesses. Please wait a minute and try again.') db.execute('INSERT INTO recognition_usage(started) VALUES(?)',(now,));db.execute('COMMIT') except Exception: db.execute('ROLLBACK');raise @contextmanager def lease(self,now=None): if not self.slots.acquire(blocking=False):raise SharedLimit('Other drawings are being recognized. Please try again in a moment.') try: self.reserve(now);yield finally:self.slots.release()