1 Commits
dev ... 0.1.1

Author SHA1 Message Date
6e131379c5 Merge pull request 'dev' (#2) from dev into main
Reviewed-on: #2
2025-05-23 16:16:10 +01:00
8 changed files with 145 additions and 353 deletions

21
.bumpversion.toml Normal file
View File

@@ -0,0 +1,21 @@
[tool.bumpversion]
current_version = "0.1.0"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
replace = "{new_version}"
regex = false
ignore_missing_version = false
ignore_missing_files = false
tag = false
sign_tags = false
tag_name = "v{new_version}"
tag_message = "Bump version: {current_version} → {new_version}"
allow_dirty = false
commit = false
message = "Bump version: {current_version} → {new_version}"
moveable_tags = []
commit_args = ""
setup_hooks = []
pre_commit_hooks = []
post_commit_hooks = []

View File

@@ -1,46 +0,0 @@
{
"categories": [
{
"title": "## 🚀 Features",
"labels": ["add","Add", "Kind/Feature"]
},
{
"title": "## 🐛 Fixes",
"labels": ["fix","Fix", "Kind/Bug"]
},
{
"title": "## 🧪 Upgrade",
"labels": ["upgrade","Upgrade","Clean"]
}
,
{
"title": "## 📝 Documentation",
"labels": ["docs","Docs", "Kind/Documentation"]
},
{
"title": "## 🧹 Chore",
"labels": ["chore","Chore", "Kind/Chore"]
},
{
"title": "## 🛠️ Maintenance",
"labels": ["maintenance","Maintenance", "Kind/Maintenance"]
},
{
"title": "## 🗑️ Deprecation",
"labels": ["deprecation","Deprecation", "Kind/Deprecation"]
}
],
"label_extractor": [
{
"pattern": "(\\w+) (.+)",
"target": "$1",
"on_property": "title"
}
],
"sort": "ASC",
"template": "${{CHANGELOG}}",
"pr_template": "- ${{TITLE}}\n - PR: #${{NUMBER}}",
"empty_template": "- no changes",
"max_pull_requests": 1000,
"max_back_track_time_days": 1000
}

View File

@@ -1,6 +1,10 @@
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
release_notes:
description: Release notes (use \n for newlines)
type: string
required: false
github_release: github_release:
description: 'Create Gitea Release' description: 'Create Gitea Release'
default: true default: true
@@ -21,15 +25,10 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@master uses: actions/checkout@master
with:
fetch-depth: 0
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:
python-version-file: "pyproject.toml"
- name: Set Git identity - name: Set Git identity
run: | run: |
git config user.name "Gitea CI" git config user.name "Gitea CI"
@@ -49,15 +48,13 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }} branch: ${{ github.ref }}
- name: Build Changelog - name: Create release notes
id: build_changelog run: |
uses: https://github.com/mikepenz/release-changelog-builder-action@v5 mkdir release_notes
with: echo -e "${{ inputs.release_notes }}" >> release_notes/release_notes.md
platform: "gitea" echo "Release notes:"
baseURL: "http://gitea:3000" cat release_notes/release_notes.md
configuration: ".gitea/changelog-config.json" echo ""
env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Build package - name: Build package
run: uv build run: uv build
- name: Publish package - name: Publish package
@@ -73,7 +70,7 @@ jobs:
with: with:
tag_name: ${{ env.VERSION }} tag_name: ${{ env.VERSION }}
release_name: Release ${{ env.VERSION }} release_name: Release ${{ env.VERSION }}
body: ${{steps.build_changelog.outputs.changelog}} body_path: release_notes/release_notes.md
draft: false draft: false
prerelease: false prerelease: false
make_latest: true make_latest: true

View File

@@ -9,8 +9,6 @@ authors = [
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"komconfig", "komconfig",
"pymysql>=1.1.1",
"sqlalchemy[asyncio]>=2.0.41",
] ]
[build-system] [build-system]
@@ -19,25 +17,3 @@ build-backend = "hatchling.build"
[tool.uv.sources] [tool.uv.sources]
komconfig = { workspace = true } komconfig = { workspace = true }
[tool.bumpversion]
current_version = "0.1.0"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
replace = "{new_version}"
regex = false
ignore_missing_version = false
ignore_missing_files = false
tag = true
sign_tags = false
tag_name = "v{new_version}"
tag_message = "Bump version: {current_version} → {new_version}"
allow_dirty = true
commit = true
message = "Bump version: {current_version} → {new_version}"
moveable_tags = []
commit_args = ""
setup_hooks = []
pre_commit_hooks = []
post_commit_hooks = []

View File

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

View File

@@ -1,240 +1,138 @@
import time import sqlite3
from typing import Any, Tuple, Union from komconfig import KomConfig, CONFIG_PATH
from pathlib import Path
import os
import loguru import loguru
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
log = loguru.logger log = loguru.logger
log.remove() log.remove()
log.add("logs/cache.log", level="INFO", rotation="15MB", retention="1 week") log.add("logs/cache.log", level="INFO", rotation="15MB", retention="1 week")
log.add("logs/cli.log", rotation="15MB", retention="1 week") # type:ignore log.add("logs/cli.log", rotation="15MB", retention="1 week") # type:ignore
config = KomConfig()
Base = declarative_base()
class KomGrabber(Base):
__tablename__ = "komgrabber"
id = Column(Integer, primary_key=True)
name = Column(String, unique=True, nullable=False)
series_id = Column(String, nullable=False)
status = Column(String, nullable=False)
created_at = Column(Date, nullable=False)
updated_at = Column(Date, nullable=False)
last_checked = Column(Date, 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 = CONFIG_PATH):
self.db_path = db_path or config.cache.path self.db_path = Path(db_path, "komcache.db")
log.debug(f"Cache path: {self.db_path}") if "~" in str(self.db_path):
if config.cache.mode == "local": self.db_path = os.path.expanduser(str(self.db_path))
self.db_path = db_path or config.cache.path self.conn = sqlite3.connect(self.db_path)
log.debug(f"Cache path: {self.db_path}") self.cursor = self.conn.cursor()
self.engine = create_engine(f"sqlite:///{self.db_path}", pool_pre_ping=True)
elif config.cache.mode == "remote":
db_url = (
config.cache.url
) # e.g., "mysql+pymysql://user:pass@host:3306/dbname"
log.debug(f"Using remote DB URL: {protect(db_url)}") def __enter__(self):
self.engine = create_engine( return self
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) def __exit__(self, exc_type, exc_val, exc_tb):
# if tables do not exist, create them self.conn.close()
if config.cache.mode == "local": pass
if not self.query(
"SELECT name FROM sqlite_master WHERE type='table' AND name='komgrabber'"
):
self.create_table()
elif config.cache.mode == "remote":
if not self.query("SHOW TABLES LIKE 'komgrabber'"):
self.create_table()
def _run(self, fn_desc: str, callable_, retries: int = 2, *args, **kwargs): def create_table(self, table_query: str) -> None:
attempt = 0 """
while True: Create a table in the database.
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): Parameters
"""Ensure all tables are created in the database.""" ----------
if config.cache.mode == "local": table_query : str
log.debug("Creating SQLite tables") the SQL query to create the table.
with self.engine.begin() as connection: """
log.debug(f"DB engine URL: {self.engine.url}") self.cursor.execute(table_query)
self.conn.commit()
for table_sql in CREATE_SQLITE_TABLES:
connection.execute(text(table_sql))
elif config.cache.mode == "remote":
log.debug("Creating MariaDB tables")
with self.engine.begin() as connection:
log.debug(f"DB engine URL: {self.engine.url}")
for table_sql in CREATE_MARIADB_TABLES:
log.debug(f"Executing table creation SQL: {table_sql}")
try:
connection.execute(text(table_sql))
except Exception as e:
log.exception(
f"Failed to execute table creation SQL:\n{table_sql}"
)
log.exception(f"Error: {e}")
# create the KomGrabber table using SQLAlchemy ORM
Base.metadata.create_all(self.engine)
def delete_table(self, table_name: str) -> bool: def delete_table(self, table_name: str) -> bool:
try: try:
with self.engine.connect() as connection: self.cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
connection.execute(text(f"DROP TABLE IF EXISTS {table_name}")) self.conn.commit()
return True return True
except SQLAlchemyError as e: except sqlite3.Error as e:
log.error(f"Error deleting table {table_name}: {e}") print(f"Error deleting table {table_name}: {e}")
return False return False
def query(self, query: str, args: dict[str, Any] = None): def query_table(self, table: str, query: str) -> list:
"""Run an arbitrary SQL statement. try:
For SELECT (or other rowreturning) statements: returns list of rows. self.cursor.execute(f"SELECT {query} FROM {table}")
For nonSELECT: executes and commits, returns []. return self.cursor.fetchall()
""" except sqlite3.Error as e:
print(f"Error querying table {table}: {e}")
return []
def query(self, query, args=None):
if args is None: if args is None:
args = {} args = []
try:
def _do(): self.cursor.execute(query, args)
session = self.Session() data = self.cursor.fetchall()
try: if data:
result = session.execute(text(query), args) return data
# SQLAlchemy 1.4/2.0: result.returns_rows tells us if rows are present else:
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()
result = self._run("query", _do)
return result or []
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:
session.execute(text(query), args)
session.commit()
return True
finally:
session.close()
return bool(self._run("insert", _do))
def update(self, query: str, args: dict[str, Any]) -> bool:
def _do():
session = self.Session()
try:
session.execute(text(query), args)
session.commit()
return True
finally:
session.close()
return bool(self._run("update", _do))
def delete(self, query: str, args: dict[str, Any]) -> bool:
def _do():
session = self.Session()
try:
session.execute(text(query), args)
session.commit()
return True
finally:
session.close()
return bool(self._run("delete", _do))
def fetch_one(self, query: str, args: dict[str, Any] = None) -> Union[Tuple, None]:
if args is None:
args = {}
def _do():
session = self.Session()
try:
result = session.execute(text(query), args)
if getattr(result, "returns_rows", False):
return result.fetchone()
return None return None
finally: except sqlite3.Error as e:
session.close() print(f"Error executing query: {e}")
return []
return self._run("fetch_one", _do) def insert(self, query: str, args=None):
def fetch_all(self, query: str, args: dict[str, Any] | None = None):
if args is None: if args is None:
args = {} args = []
try:
self.cursor.execute(query, args)
self.conn.commit()
return True
except sqlite3.Error as e:
print(f"Error inserting data: {e}")
return False
def _do(): def update(self, query, args=None):
session = self.Session() if args is None:
try: args = []
result = session.execute(text(query), args) try:
if getattr(result, "returns_rows", False): self.cursor.execute(query, args)
return result.fetchall() log.debug("Query: {}, Args: {}".format(query, args))
return [] self.conn.commit()
finally: return True
session.close() except sqlite3.Error as e:
print(f"Error updating data: {e}")
return False
result = self._run("fetch_all", _do) def get_last_update_date(self, series_name: str) -> str:
return result or [] """
Get the last update date for a series.
Parameters
----------
series_name : str
The name of the series.
Returns
-------
str
The last update date.
"""
query = "SELECT last_update_date FROM komgrabber WHERE series_name = ?"
result = self.query(query, (series_name,))
if result:
return result[0][0]
return ""
def fetch_one(self, query: str, args=None):
if args is None:
args = []
try:
self.cursor.execute(query, args)
return self.cursor.fetchone()
except sqlite3.Error as e:
print(f"Error fetching one: {e}")
return None
def fetch_all(self, query: str, args=None):
if args is None:
args = []
try:
self.cursor.execute(query, args)
return self.cursor.fetchall()
except sqlite3.Error as e:
print(f"Error fetching one: {e}")
return None
if __name__ == "__main__": if __name__ == "__main__":
cache = KomCache() # Example usage
cache.create_table() with KomCache() as cache:
cache.delete_table("komgrabber")

View File

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

View File

@@ -1,20 +0,0 @@
CREATE_SQLITE_TABLES = ["""
CREATE TABLE IF NOT EXISTS manga_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
manga_id INTEGER,
grabbed BOOLEAN DEFAULT 0);""","""CREATE TABLE IF NOT EXISTS komgrabber (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
series_id TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_checked TIMESTAMP DEFAULT 0,
completed BOOLEAN DEFAULT 0);""","""CREATE TABLE IF NOT EXISTS komtagger (
id INTEGER PRIMARY KEY AUTOINCREMENT,
series_id TEXT NOT NULL,
title TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_checked TIMESTAMP DEFAULT 0,
status TEXT NOT NULL);
"""]