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")
|
cloud_mode = os.environ.get("CLOUD_MODE", "").lower() in ("1", "true", "yes")
|
||||||
if cloud_mode and key:
|
if cloud_mode and key:
|
||||||
from pysqlcipher3 import dbapi2 as _sqlcipher # type: ignore
|
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}'")
|
conn.execute(f"PRAGMA key='{key}'")
|
||||||
return conn
|
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:
|
if sql_file.name in applied:
|
||||||
continue
|
continue
|
||||||
conn.executescript(sql_file.read_text())
|
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()
|
conn.commit()
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ def can_use(
|
||||||
has_byok: bool = False,
|
has_byok: bool = False,
|
||||||
has_local_vision: bool = False,
|
has_local_vision: bool = False,
|
||||||
_features: dict[str, str] | None = None,
|
_features: dict[str, str] | None = None,
|
||||||
|
_byok_unlockable: frozenset[str] | None = None,
|
||||||
|
_local_vision_unlockable: frozenset[str] | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if the given tier (and optional unlocks) can access feature.
|
Return True if the given tier (and optional unlocks) can access feature.
|
||||||
|
|
@ -41,15 +43,22 @@ def can_use(
|
||||||
has_local_vision: True if user has a local vision model configured.
|
has_local_vision: True if user has a local vision model configured.
|
||||||
_features: Feature→min_tier map. Products pass their own dict here.
|
_features: Feature→min_tier map. Products pass their own dict here.
|
||||||
If None, all features are free.
|
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 {}
|
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:
|
if feature not in features:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if has_byok and feature in BYOK_UNLOCKABLE:
|
if has_byok and feature in byok_unlockable:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if has_local_vision and feature in LOCAL_VISION_UNLOCKABLE:
|
if has_local_vision and feature in local_vision_unlockable:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
min_tier = features[feature]
|
min_tier = features[feature]
|
||||||
|
|
@ -64,13 +73,18 @@ def tier_label(
|
||||||
has_byok: bool = False,
|
has_byok: bool = False,
|
||||||
has_local_vision: bool = False,
|
has_local_vision: bool = False,
|
||||||
_features: dict[str, str] | None = None,
|
_features: dict[str, str] | None = None,
|
||||||
|
_byok_unlockable: frozenset[str] | None = None,
|
||||||
|
_local_vision_unlockable: frozenset[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Return a human-readable label for the minimum tier needed for feature."""
|
"""Return a human-readable label for the minimum tier needed for feature."""
|
||||||
features = _features or {}
|
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:
|
if feature not in features:
|
||||||
return "free"
|
return "free"
|
||||||
if has_byok and feature in BYOK_UNLOCKABLE:
|
if has_byok and feature in byok_unlockable:
|
||||||
return "free (BYOK)"
|
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 "free (local vision)"
|
||||||
return features[feature]
|
return features[feature]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue