fix(core): SQLite timeout=30, INSERT OR IGNORE migrations, parameterize tier unlockables
- get_connection(): add timeout=30 to both sqlite3 and pysqlcipher3 paths so concurrent writers retry instead of immediately raising OperationalError - run_migrations(): INSERT OR IGNORE so two Store() calls racing on first boot don't hit a UNIQUE constraint on the migrations table - can_use() / tier_label(): accept _byok_unlockable and _local_vision_unlockable overrides so products pass their own frozensets rather than sharing module-level constants (required for circuitforge-core to serve multiple products cleanly)
This commit is contained in:
parent
22bad8590a
commit
c027fe6137
3 changed files with 31 additions and 13 deletions
|
|
@ -22,7 +22,9 @@ def get_connection(db_path: Path, key: str = "") -> sqlite3.Connection:
|
|||
cloud_mode = os.environ.get("CLOUD_MODE", "").lower() in ("1", "true", "yes")
|
||||
if cloud_mode and key:
|
||||
from pysqlcipher3 import dbapi2 as _sqlcipher # type: ignore
|
||||
conn = _sqlcipher.connect(str(db_path))
|
||||
conn = _sqlcipher.connect(str(db_path), timeout=30)
|
||||
conn.execute(f"PRAGMA key='{key}'")
|
||||
return conn
|
||||
return sqlite3.connect(str(db_path))
|
||||
# timeout=30: retry for up to 30s when another writer holds the lock (WAL mode
|
||||
# allows concurrent readers but only one writer at a time).
|
||||
return sqlite3.connect(str(db_path), timeout=30)
|
||||
|
|
|
|||
|
|
@ -23,5 +23,7 @@ def run_migrations(conn: sqlite3.Connection, migrations_dir: Path) -> None:
|
|||
if sql_file.name in applied:
|
||||
continue
|
||||
conn.executescript(sql_file.read_text())
|
||||
conn.execute("INSERT INTO _migrations (name) VALUES (?)", (sql_file.name,))
|
||||
# OR IGNORE: safe if two Store() calls race on the same DB — second writer
|
||||
# just skips the insert rather than raising UNIQUE constraint failed.
|
||||
conn.execute("INSERT OR IGNORE INTO _migrations (name) VALUES (?)", (sql_file.name,))
|
||||
conn.commit()
|
||||
|
|
|
|||
|
|
@ -30,26 +30,35 @@ def can_use(
|
|||
has_byok: bool = False,
|
||||
has_local_vision: bool = False,
|
||||
_features: dict[str, str] | None = None,
|
||||
_byok_unlockable: frozenset[str] | None = None,
|
||||
_local_vision_unlockable: frozenset[str] | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Return True if the given tier (and optional unlocks) can access feature.
|
||||
|
||||
Args:
|
||||
feature: Feature key string.
|
||||
tier: User's current tier ("free", "paid", "premium", "ultra").
|
||||
has_byok: True if user has a configured LLM backend.
|
||||
has_local_vision: True if user has a local vision model configured.
|
||||
_features: Feature→min_tier map. Products pass their own dict here.
|
||||
If None, all features are free.
|
||||
feature: Feature key string.
|
||||
tier: User's current tier ("free", "paid", "premium", "ultra").
|
||||
has_byok: True if user has a configured LLM backend.
|
||||
has_local_vision: True if user has a local vision model configured.
|
||||
_features: Feature→min_tier map. Products pass their own dict here.
|
||||
If None, all features are free.
|
||||
_byok_unlockable: Product-specific BYOK-unlockable features.
|
||||
If None, uses module-level BYOK_UNLOCKABLE.
|
||||
_local_vision_unlockable: Product-specific local vision unlockable features.
|
||||
If None, uses module-level LOCAL_VISION_UNLOCKABLE.
|
||||
"""
|
||||
features = _features or {}
|
||||
byok_unlockable = _byok_unlockable if _byok_unlockable is not None else BYOK_UNLOCKABLE
|
||||
local_vision_unlockable = _local_vision_unlockable if _local_vision_unlockable is not None else LOCAL_VISION_UNLOCKABLE
|
||||
|
||||
if feature not in features:
|
||||
return True
|
||||
|
||||
if has_byok and feature in BYOK_UNLOCKABLE:
|
||||
if has_byok and feature in byok_unlockable:
|
||||
return True
|
||||
|
||||
if has_local_vision and feature in LOCAL_VISION_UNLOCKABLE:
|
||||
if has_local_vision and feature in local_vision_unlockable:
|
||||
return True
|
||||
|
||||
min_tier = features[feature]
|
||||
|
|
@ -64,13 +73,18 @@ def tier_label(
|
|||
has_byok: bool = False,
|
||||
has_local_vision: bool = False,
|
||||
_features: dict[str, str] | None = None,
|
||||
_byok_unlockable: frozenset[str] | None = None,
|
||||
_local_vision_unlockable: frozenset[str] | None = None,
|
||||
) -> str:
|
||||
"""Return a human-readable label for the minimum tier needed for feature."""
|
||||
features = _features or {}
|
||||
byok_unlockable = _byok_unlockable if _byok_unlockable is not None else BYOK_UNLOCKABLE
|
||||
local_vision_unlockable = _local_vision_unlockable if _local_vision_unlockable is not None else LOCAL_VISION_UNLOCKABLE
|
||||
|
||||
if feature not in features:
|
||||
return "free"
|
||||
if has_byok and feature in BYOK_UNLOCKABLE:
|
||||
if has_byok and feature in byok_unlockable:
|
||||
return "free (BYOK)"
|
||||
if has_local_vision and feature in LOCAL_VISION_UNLOCKABLE:
|
||||
if has_local_vision and feature in local_vision_unlockable:
|
||||
return "free (local vision)"
|
||||
return features[feature]
|
||||
|
|
|
|||
Loading…
Reference in a new issue