h
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -243,4 +243,5 @@ authtoken.secret
|
|||||||
|
|
||||||
mypy.
|
mypy.
|
||||||
.idea
|
.idea
|
||||||
medios.db
|
medios.db
|
||||||
|
medios*
|
||||||
2
CLI.py
2
CLI.py
@@ -19,7 +19,7 @@ if not os.environ.get("MM_DEBUG"):
|
|||||||
db_path = Path(__file__).resolve().parent / "medios.db"
|
db_path = Path(__file__).resolve().parent / "medios.db"
|
||||||
if db_path.exists():
|
if db_path.exists():
|
||||||
import sqlite3
|
import sqlite3
|
||||||
with sqlite3.connect(str(db_path)) as conn:
|
with sqlite3.connect(str(db_path), timeout=30.0) as conn:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
# Check for global debug key
|
# Check for global debug key
|
||||||
cur.execute("SELECT value FROM config WHERE key = 'debug' AND category = 'global'")
|
cur.execute("SELECT value FROM config WHERE key = 'debug' AND category = 'global'")
|
||||||
|
|||||||
123
SYS/config.py
123
SYS/config.py
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional, List
|
from typing import Any, Dict, Optional, List
|
||||||
from SYS.logger import log
|
from SYS.logger import log
|
||||||
@@ -39,80 +40,6 @@ def clear_config_cache() -> None:
|
|||||||
_CONFIG_CACHE.clear()
|
_CONFIG_CACHE.clear()
|
||||||
|
|
||||||
|
|
||||||
def reload_config(
|
|
||||||
config_dir: Optional[Path] = None, filename: str = "medios.db"
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
_CONFIG_CACHE.pop("db_config", None)
|
|
||||||
return load_config(config_dir=config_dir, filename=filename)
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(
|
|
||||||
config_dir: Optional[Path] = None, filename: str = "medios.db"
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
# We no longer use config_dir or filename for the config file itself,
|
|
||||||
# but we keep them in the signature for backward compatibility.
|
|
||||||
cache_key = "db_config"
|
|
||||||
|
|
||||||
if cache_key in _CONFIG_CACHE:
|
|
||||||
return _CONFIG_CACHE[cache_key]
|
|
||||||
|
|
||||||
# Load from database
|
|
||||||
try:
|
|
||||||
from SYS.database import get_config_all
|
|
||||||
db_config = get_config_all()
|
|
||||||
if db_config:
|
|
||||||
_CONFIG_CACHE[cache_key] = db_config
|
|
||||||
return db_config
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def save_config(
|
|
||||||
config: Dict[str, Any],
|
|
||||||
config_dir: Optional[Path] = None,
|
|
||||||
filename: str = "medios.db",
|
|
||||||
) -> None:
|
|
||||||
"""Persist configuration to the database."""
|
|
||||||
try:
|
|
||||||
from SYS.database import save_config_value
|
|
||||||
|
|
||||||
for key, value in config.items():
|
|
||||||
if key in ('store', 'provider', 'tool'):
|
|
||||||
if isinstance(value, dict):
|
|
||||||
for subtype, instances in value.items():
|
|
||||||
if isinstance(instances, dict):
|
|
||||||
# provider/tool are usually config[cat][subtype][key]
|
|
||||||
# but store is config['store'][subtype][name][key]
|
|
||||||
if key == 'store':
|
|
||||||
for name, settings in instances.items():
|
|
||||||
if isinstance(settings, dict):
|
|
||||||
for k, v in settings.items():
|
|
||||||
save_config_value(key, subtype, name, k, v)
|
|
||||||
else:
|
|
||||||
for k, v in instances.items():
|
|
||||||
save_config_value(key, subtype, "default", k, v)
|
|
||||||
else:
|
|
||||||
# global settings
|
|
||||||
if not key.startswith("_"):
|
|
||||||
save_config_value("global", "none", "none", key, value)
|
|
||||||
except Exception as e:
|
|
||||||
log(f"Failed to save config to database: {e}")
|
|
||||||
|
|
||||||
_CONFIG_CACHE["db_config"] = config
|
|
||||||
|
|
||||||
|
|
||||||
def load() -> Dict[str, Any]:
|
|
||||||
"""Return the parsed configuration from database."""
|
|
||||||
return load_config()
|
|
||||||
|
|
||||||
|
|
||||||
def save(config: Dict[str, Any]) -> None:
|
|
||||||
"""Persist *config* back to database."""
|
|
||||||
save_config(config)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_cache_key(config_dir: Optional[Path], filename: str, actual_path: Optional[Path]) -> str:
|
def _make_cache_key(config_dir: Optional[Path], filename: str, actual_path: Optional[Path]) -> str:
|
||||||
if actual_path:
|
if actual_path:
|
||||||
return str(actual_path.resolve())
|
return str(actual_path.resolve())
|
||||||
@@ -436,6 +363,11 @@ def _validate_config_safety(config: Dict[str, Any]) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_conf(config: Dict[str, Any]) -> str:
|
||||||
|
"""Serialize configuration to a string for legacy .conf files."""
|
||||||
|
return json.dumps(config, indent=4)
|
||||||
|
|
||||||
|
|
||||||
def save_config(
|
def save_config(
|
||||||
config: Dict[str, Any],
|
config: Dict[str, Any],
|
||||||
config_dir: Optional[Path] = None,
|
config_dir: Optional[Path] = None,
|
||||||
@@ -448,27 +380,28 @@ def save_config(
|
|||||||
try:
|
try:
|
||||||
from SYS.database import db, save_config_value
|
from SYS.database import db, save_config_value
|
||||||
|
|
||||||
# We want to clear and re-save or just update?
|
with db.transaction():
|
||||||
# For simplicity, we'll iterate and update.
|
# We want to clear and re-save or just update?
|
||||||
for key, value in config.items():
|
# For simplicity, we'll iterate and update.
|
||||||
if key in ('store', 'provider', 'tool'):
|
for key, value in config.items():
|
||||||
if isinstance(value, dict):
|
if key in ('store', 'provider', 'tool'):
|
||||||
for subtype, instances in value.items():
|
if isinstance(value, dict):
|
||||||
if isinstance(instances, dict):
|
for subtype, instances in value.items():
|
||||||
# provider/tool are usually config[cat][subtype][key]
|
if isinstance(instances, dict):
|
||||||
# but store is config['store'][subtype][name][key]
|
# provider/tool are usually config[cat][subtype][key]
|
||||||
if key == 'store':
|
# but store is config['store'][subtype][name][key]
|
||||||
for name, settings in instances.items():
|
if key == 'store':
|
||||||
if isinstance(settings, dict):
|
for name, settings in instances.items():
|
||||||
for k, v in settings.items():
|
if isinstance(settings, dict):
|
||||||
save_config_value(key, subtype, name, k, v)
|
for k, v in settings.items():
|
||||||
else:
|
save_config_value(key, subtype, name, k, v)
|
||||||
for k, v in instances.items():
|
else:
|
||||||
save_config_value(key, subtype, "default", k, v)
|
for k, v in instances.items():
|
||||||
else:
|
save_config_value(key, subtype, "default", k, v)
|
||||||
# global settings
|
else:
|
||||||
if not key.startswith("_"):
|
# global settings
|
||||||
save_config_value("global", "none", "none", key, value)
|
if not key.startswith("_"):
|
||||||
|
save_config_value("global", "none", "none", key, value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"Failed to save config to database: {e}")
|
log(f"Failed to save config to database: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import sqlite3
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
# The database is located in the project root
|
# The database is located in the project root
|
||||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||||
@@ -19,8 +20,20 @@ class Database:
|
|||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def _init_db(self):
|
def _init_db(self):
|
||||||
self.conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
|
self.conn = sqlite3.connect(
|
||||||
|
str(DB_PATH),
|
||||||
|
check_same_thread=False,
|
||||||
|
timeout=30.0 # Increase timeout to 30s to avoid locking issues
|
||||||
|
)
|
||||||
self.conn.row_factory = sqlite3.Row
|
self.conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
# Use WAL mode for better concurrency (allows multiple readers + 1 writer)
|
||||||
|
try:
|
||||||
|
self.conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
self.conn.execute("PRAGMA synchronous=NORMAL")
|
||||||
|
except sqlite3.Error:
|
||||||
|
pass
|
||||||
|
|
||||||
self._create_tables()
|
self._create_tables()
|
||||||
|
|
||||||
def _create_tables(self):
|
def _create_tables(self):
|
||||||
@@ -89,19 +102,58 @@ class Database:
|
|||||||
|
|
||||||
def execute(self, query: str, params: tuple = ()):
|
def execute(self, query: str, params: tuple = ()):
|
||||||
cursor = self.conn.cursor()
|
cursor = self.conn.cursor()
|
||||||
cursor.execute(query, params)
|
try:
|
||||||
self.conn.commit()
|
cursor.execute(query, params)
|
||||||
return cursor
|
if not self.conn.in_transaction:
|
||||||
|
self.conn.commit()
|
||||||
|
return cursor
|
||||||
|
except Exception:
|
||||||
|
if not self.conn.in_transaction:
|
||||||
|
self.conn.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def executemany(self, query: str, param_list: List[tuple]):
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.executemany(query, param_list)
|
||||||
|
if not self.conn.in_transaction:
|
||||||
|
self.conn.commit()
|
||||||
|
return cursor
|
||||||
|
except Exception:
|
||||||
|
if not self.conn.in_transaction:
|
||||||
|
self.conn.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def transaction(self):
|
||||||
|
"""Context manager for a database transaction."""
|
||||||
|
if self.conn.in_transaction:
|
||||||
|
# Already in a transaction, just yield
|
||||||
|
yield self.conn
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.conn.execute("BEGIN")
|
||||||
|
yield self.conn
|
||||||
|
self.conn.commit()
|
||||||
|
except Exception:
|
||||||
|
self.conn.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
def fetchall(self, query: str, params: tuple = ()):
|
def fetchall(self, query: str, params: tuple = ()):
|
||||||
cursor = self.conn.cursor()
|
cursor = self.conn.cursor()
|
||||||
cursor.execute(query, params)
|
try:
|
||||||
return cursor.fetchall()
|
cursor.execute(query, params)
|
||||||
|
return cursor.fetchall()
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
def fetchone(self, query: str, params: tuple = ()):
|
def fetchone(self, query: str, params: tuple = ()):
|
||||||
cursor = self.conn.cursor()
|
cursor = self.conn.cursor()
|
||||||
cursor.execute(query, params)
|
try:
|
||||||
return cursor.fetchone()
|
cursor.execute(query, params)
|
||||||
|
return cursor.fetchone()
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
db = Database()
|
db = Database()
|
||||||
|
|||||||
@@ -751,7 +751,7 @@ def main() -> int:
|
|||||||
if db_path.exists():
|
if db_path.exists():
|
||||||
try:
|
try:
|
||||||
import sqlite3
|
import sqlite3
|
||||||
with sqlite3.connect(str(db_path)) as conn:
|
with sqlite3.connect(str(db_path), timeout=30.0) as conn:
|
||||||
# We want to set store.hydrusnetwork.hydrus.<key>
|
# We want to set store.hydrusnetwork.hydrus.<key>
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
# Check if hydrusnetwork store exists
|
# Check if hydrusnetwork store exists
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ def update_medios_config(hydrus_path: Path) -> bool:
|
|||||||
try:
|
try:
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import json
|
import json
|
||||||
with sqlite3.connect(str(db_path)) as conn:
|
with sqlite3.connect(str(db_path), timeout=30.0) as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
|||||||
@@ -791,7 +791,19 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
# and the user did not explicitly pass --venv. This matches the user's likely
|
# and the user did not explicitly pass --venv. This matches the user's likely
|
||||||
# intent when they called: <venv_python> scripts/run_client.py ...
|
# intent when they called: <venv_python> scripts/run_client.py ...
|
||||||
cur_py = Path(sys.executable)
|
cur_py = Path(sys.executable)
|
||||||
if args.venv is None and _is_running_in_virtualenv() and cur_py:
|
|
||||||
|
# However, if we've already found a repo-local venv and the current Python
|
||||||
|
# is external to the repository, we do NOT prefer it yet - we'll verify the
|
||||||
|
# repo-local one first. This prevents tools like Medios-Macina from
|
||||||
|
# accidentally installing their own venv into the repo's services.
|
||||||
|
cur_is_external = True
|
||||||
|
try:
|
||||||
|
if repo_root in cur_py.resolve().parents:
|
||||||
|
cur_is_external = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if args.venv is None and _is_running_in_virtualenv() and cur_py and (not venv_py or not cur_is_external):
|
||||||
# If current interpreter looks like a venv and can import required modules,
|
# If current interpreter looks like a venv and can import required modules,
|
||||||
# prefer it immediately rather than forcing the repo venv.
|
# prefer it immediately rather than forcing the repo venv.
|
||||||
req = find_requirements(repo_root)
|
req = find_requirements(repo_root)
|
||||||
|
|||||||
Reference in New Issue
Block a user