6 Commits
dev ... v0.1.2

5 changed files with 83 additions and 156 deletions

View File

@@ -26,8 +26,8 @@ jobs:
fetch-tags: true fetch-tags: true
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v5
- name: "Set up Python" - name: Set up Python
uses: actions/setup-python@v6 run: uv python install
with: with:
python-version-file: "pyproject.toml" python-version-file: "pyproject.toml"
- name: Set Git identity - name: Set Git identity

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "komcache" name = "komcache"
version = "0.1.0" version = "0.1.2"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
authors = [ authors = [
@@ -21,7 +21,7 @@ build-backend = "hatchling.build"
komconfig = { workspace = true } komconfig = { workspace = true }
[tool.bumpversion] [tool.bumpversion]
current_version = "0.1.0" current_version = "0.1.2"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)" parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"] serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}" search = "{current_version}"

View File

@@ -1,2 +1 @@
__all__ = ["KomCache"]
from .cache import KomCache from .cache import KomCache

View File

@@ -1,14 +1,11 @@
import time from typing import Any
from typing import Any, Tuple, Union from sqlalchemy import create_engine, Column, String, Integer, Date, text
from sqlalchemy.orm import sessionmaker, declarative_base
import loguru from sqlalchemy.exc import SQLAlchemyError
from komconfig import KomConfig
from sqlalchemy import Column, Date, Integer, String, create_engine, text
from sqlalchemy.exc import OperationalError, SQLAlchemyError
from sqlalchemy.orm import declarative_base, sessionmaker
from komcache.schemas.mariadb import CREATE_MARIADB_TABLES
from komcache.schemas.sqlite import CREATE_SQLITE_TABLES from komcache.schemas.sqlite import CREATE_SQLITE_TABLES
from komcache.schemas.mariadb import CREATE_MARIADB_TABLES
from komconfig import KomConfig
import loguru
log = loguru.logger log = loguru.logger
log.remove() log.remove()
@@ -31,10 +28,6 @@ class KomGrabber(Base):
complete = Column(Integer, nullable=False) complete = Column(Integer, nullable=False)
def protect(url: str) -> str:
return "mysql+pymysql://user:pass@host:3306/dbname"
class KomCache: class KomCache:
def __init__(self, db_path: str = ""): # Default to empty string if not provided def __init__(self, db_path: str = ""): # Default to empty string if not provided
self.db_path = db_path or config.cache.path self.db_path = db_path or config.cache.path
@@ -42,24 +35,15 @@ class KomCache:
if config.cache.mode == "local": if config.cache.mode == "local":
self.db_path = db_path or config.cache.path self.db_path = db_path or config.cache.path
log.debug(f"Cache path: {self.db_path}") log.debug(f"Cache path: {self.db_path}")
self.engine = create_engine(f"sqlite:///{self.db_path}", pool_pre_ping=True) self.engine = create_engine(f"sqlite:///{self.db_path}")
elif config.cache.mode == "remote": elif config.cache.mode == "remote":
db_url = ( db_url = (
config.cache.url config.cache.url
) # e.g., "mysql+pymysql://user:pass@host:3306/dbname" ) # e.g., "mysql+pymysql://user:pass@host:3306/dbname"
log.debug(f"Using remote DB URL: {db_url}")
self.engine = create_engine(db_url)
log.debug(f"Using remote DB URL: {protect(db_url)}") self.Session = sessionmaker(bind=self.engine)
self.engine = create_engine(
db_url,
pool_pre_ping=True,
pool_recycle=1800,
pool_size=5,
max_overflow=10,
pool_timeout=30,
connect_args={"connect_timeout": 10},
)
self.Session = sessionmaker(bind=self.engine, expire_on_commit=False)
# if tables do not exist, create them # if tables do not exist, create them
if config.cache.mode == "local": if config.cache.mode == "local":
if not self.query( if not self.query(
@@ -70,29 +54,6 @@ class KomCache:
if not self.query("SHOW TABLES LIKE 'komgrabber'"): if not self.query("SHOW TABLES LIKE 'komgrabber'"):
self.create_table() self.create_table()
def _run(self, fn_desc: str, callable_, retries: int = 2, *args, **kwargs):
attempt = 0
while True:
try:
return callable_(*args, **kwargs)
except OperationalError as e:
# MySQL server has gone away (2006) / Lost connection (2013)
if attempt < retries and any(
code in str(e.orig) for code in ("2006", "2013")
):
attempt += 1
wait = 1 * attempt
log.warning(
f"{fn_desc} failed due to connection loss (attempt {attempt}/{retries}). Retrying in {wait}s."
)
time.sleep(wait)
continue
log.error(f"{fn_desc} failed with OperationalError: {e}")
return None
except SQLAlchemyError as e:
log.error(f"{fn_desc} failed with SQLAlchemyError: {e}")
return None
def create_table(self): def create_table(self):
"""Ensure all tables are created in the database.""" """Ensure all tables are created in the database."""
if config.cache.mode == "local": if config.cache.mode == "local":
@@ -129,110 +90,87 @@ class KomCache:
return False return False
def query(self, query: str, args: dict[str, Any] = None): def query(self, query: str, args: dict[str, Any] = None):
"""Run an arbitrary SQL statement.
For SELECT (or other rowreturning) statements: returns list of rows.
For nonSELECT: executes and commits, returns [].
"""
if args is None: if args is None:
args = {} args = {}
def _do():
session = self.Session()
try: try:
result = session.execute(text(query), args) session = self.Session()
# SQLAlchemy 1.4/2.0: result.returns_rows tells us if rows are present result = session.execute(text(query), args).fetchall()
if getattr(result, "returns_rows", False):
rows = result.fetchall()
return rows
# Non-row statements: commit if they mutate
first = query.lstrip().split(None, 1)[0].upper()
if first in {
"INSERT",
"UPDATE",
"DELETE",
"REPLACE",
"ALTER",
"CREATE",
"DROP",
"TRUNCATE",
}:
session.commit()
return []
finally:
session.close() session.close()
return result
result = self._run("query", _do) except SQLAlchemyError as e:
return result or [] log.error(f"Error executing query: {e}")
return []
def insert(self, query: str, args: dict[str, Any]) -> bool: def insert(self, query: str, args: dict[str, Any]) -> bool:
# (Optionally you can now just call self.query(query, args))
def _do():
session = self.Session()
try: try:
session = self.Session()
session.execute(text(query), args) session.execute(text(query), args)
session.commit() session.commit()
return True
finally:
session.close() session.close()
return True
return bool(self._run("insert", _do)) except SQLAlchemyError as e:
log.error(f"Error inserting data: {e}")
return False
def update(self, query: str, args: dict[str, Any]) -> bool: def update(self, query: str, args: dict[str, Any]) -> bool:
def _do():
session = self.Session()
try: try:
session = self.Session()
session.execute(text(query), args) session.execute(text(query), args)
session.commit() session.commit()
return True
finally:
session.close() session.close()
return True
return bool(self._run("update", _do)) except SQLAlchemyError as e:
log.error(f"Error updating data: {e}")
return False
def delete(self, query: str, args: dict[str, Any]) -> bool: def delete(self, query: str, args: dict[str, Any]) -> bool:
def _do():
session = self.Session()
try: try:
session = self.Session()
session.execute(text(query), args) session.execute(text(query), args)
session.commit() session.commit()
return True
finally:
session.close() session.close()
return True
except SQLAlchemyError as e:
log.error(f"Error deleting data: {e}")
return False
return bool(self._run("delete", _do)) def get_last_update_date(self, series_name: str) -> str:
try:
session = self.Session()
result = (
session.query(KomGrabber.last_update_date)
.filter_by(series_name=series_name)
.first()
)
session.close()
return result[0] if result else ""
except SQLAlchemyError as e:
log.error(f"Error fetching last update date: {e}")
return ""
def fetch_one(self, query: str, args: dict[str, Any] = None) -> Union[Tuple, None]: def fetch_one(self, query: str, args: dict[str, Any] = None):
if args is None: if args is None:
args = {} args = {}
def _do():
session = self.Session()
try: try:
result = session.execute(text(query), args) session = self.Session()
if getattr(result, "returns_rows", False): result = session.execute(text(query), args).fetchone()
return result.fetchone()
return None
finally:
session.close() session.close()
return result
return self._run("fetch_one", _do) except SQLAlchemyError as e:
log.error(f"Error executing query: {e}")
return None
def fetch_all(self, query: str, args: dict[str, Any] | None = None): def fetch_all(self, query: str, args: dict[str, Any] | None = None):
if args is None: if args is None:
args = {} args = {}
def _do():
session = self.Session()
try: try:
result = session.execute(text(query), args) session = self.Session()
if getattr(result, "returns_rows", False): result = session.execute(text(query), args).fetchall()
return result.fetchall()
return []
finally:
session.close() session.close()
return result
result = self._run("fetch_all", _do) except SQLAlchemyError as e:
return result or [] log.error(f"Error executing query: {e}")
return []
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,13 +1,9 @@
CREATE_MARIADB_TABLES = [ CREATE_MARIADB_TABLES = ["""
"""
CREATE TABLE IF NOT EXISTS manga_requests ( CREATE TABLE IF NOT EXISTS manga_requests (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
manga_id INT, manga_id INT,
grabbed TINYINT(1) DEFAULT 0, grabbed TINYINT(1) DEFAULT 0
image TEXT NOT NULL, );""","""CREATE TABLE IF NOT EXISTS komgrabber (
title TEXT NOT NULL
);""",
"""CREATE TABLE IF NOT EXISTS komgrabber (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
series_id TEXT NOT NULL, series_id TEXT NOT NULL,
@@ -16,18 +12,12 @@ CREATE TABLE IF NOT EXISTS manga_requests (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_checked TIMESTAMP DEFAULT NULL, last_checked TIMESTAMP DEFAULT NULL,
completed TINYINT(1) DEFAULT 0 completed TINYINT(1) DEFAULT 0
);""", );""","""CREATE TABLE IF NOT EXISTS komtagger (
"""CREATE TABLE IF NOT EXISTS komtagger (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
series_id TEXT NOT NULL, series_id TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_checked DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00', last_checked TIMESTAMP DEFAULT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, status TEXT NOT NULL
UNIQUE KEY unique_series_id (series_id(255)),
status TEXT NOT NULL,
tag_status TEXT NOT NULL DEFAULT 'untagged',
anilist_id INT DEFAULT NULL
); );
""", """]
]