24 Commits
v0.1.9 ... main

Author SHA1 Message Date
71e59a4c09 Merge pull request 'Configure Renovate' (#17) from renovate/configure into main
Reviewed-on: #17
2025-11-27 17:11:16 +00:00
a165181462 Add renovate.json 2025-11-27 17:09:05 +00:00
Gitea CI
bead1b2291 Bump version: 0.2.4 → 0.2.5 2025-11-01 22:05:15 +00:00
36aeba08ec Merge pull request 'feat: add __main__ function with generic config retrieval' (#16) from dev into main
Reviewed-on: #16
2025-11-01 22:01:08 +00:00
86e7d9fe57 feat: add __main__ function with generic config retrieval 2025-11-01 23:00:48 +01:00
Gitea CI
423277cc7a Bump version: 0.2.3 → 0.2.4 2025-11-01 21:55:55 +00:00
26b694999f Merge pull request 'change gitea url in changelog config' (#15) from dev into main
Reviewed-on: #15
2025-11-01 21:52:37 +00:00
c07d0ee919 maintenance: update gitea url 2025-11-01 22:50:03 +01:00
Gitea CI
e16caa6473 Bump version: 0.2.2 → 0.2.3 2025-11-01 21:47:31 +00:00
382aac8d51 feat: add changelog config 2025-11-01 22:19:47 +01:00
1c9cf2065d Merge pull request 'dev' (#14) from dev into main
Reviewed-on: #14
2025-11-01 21:18:57 +00:00
bb3b922325 refactor: rework configuration models, add CLI, tests and VSCode settings
- Replace hardcoded CONFIG_PATH with appdirs AppDirs (use app.user_config_dir) and update SETTINGS_PATH
- Rework dataclasses: add Library, QbitTorrent; convert Komga.libraries to List[Library]; use Path for media_path; add getLibraryByName and proper __post_init__ conversions
- Extend KomGrabber to support downloader/downloader_settings (aria2/qbit), normalize Path handling and expanduser usage
- Add type fixes, utility methods (getattr/_setattr) and API __post_init__ to convert nested dicts to dataclass instances
- Add package CLI entrypoints (__main__.py, src package main) and simple runner (main.py)
- Add tests for package __init__ CLI and config behavior (tests/*) and a small test script (test.py)
- Add .vscode/settings.json for pytest integration
- Tidy pyproject.toml: format dependencies, add dev/test dependency groups and fix trailing newline in bumpversion section
2025-11-01 22:17:50 +01:00
674e9f9fd5 update workflow 2025-05-29 11:39:51 +02:00
Gitea CI
f67ebededd Bump version: 0.2.1 → 0.2.2 2025-05-29 09:35:36 +00:00
79846dfed5 Merge pull request 'update workflow, adjust release to include v to release tag' (#13) from dev into main
Reviewed-on: #13
2025-05-29 10:35:14 +01:00
1f306cce53 update workflow, adjust release to include v to release tag 2025-05-29 11:34:51 +02:00
Gitea CI
9b20484753 Bump version: 0.2.0 → 0.2.1 2025-05-29 09:32:44 +00:00
e5ede589e7 Merge pull request 'dev to main' (#12) from dev into main
Reviewed-on: #12
2025-05-29 10:32:23 +01:00
c7089edfdc Delete .bumpversion.toml 2025-05-29 10:31:28 +01:00
1ed1403c43 allow dirty bumps, manually adjust version to match remote version 2025-05-29 11:30:24 +02:00
40d6e8824e move bumpversion to pyproject, update workflow, add database configuration to config class 2025-05-29 11:28:55 +02:00
Gitea CI
d4ef36a54b Bump version: 0.1.9 → 0.2.0 2025-05-23 14:52:49 +00:00
8a0b73c60d Merge pull request 'refactor: update release notes handling in build workflow' (#10) from dev into main
Reviewed-on: #10
2025-05-23 15:50:59 +01:00
c9d6d5de8f refactor: update release notes handling in build workflow 2025-05-23 16:50:41 +02:00
13 changed files with 430 additions and 70 deletions

View File

@@ -1,24 +0,0 @@
[tool.bumpversion]
current_version = "0.1.9"
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 = false
commit = true
message = "Bump version: {current_version} → {new_version}"
moveable_tags = []
commit_args = ""
setup_hooks = []
pre_commit_hooks = []
post_commit_hooks = []
[[tool.bumpversion.files]]
filename = "pyproject.toml"

View File

@@ -0,0 +1,103 @@
{
"categories": [
{
"title": "## 🚀 Features",
"labels": [
"add",
"Add",
"Kind/Feature",
"feat",
"Feature",
"Feat"
]
},
{
"title": "## 🧰 Enhancements",
"labels": [
"enhancement",
"Enhancement",
"Kind/Enhancement",
"improvement",
"Improvement",
"Kind/Improvement"
]
},
{
"title": "## 🐛 Fixes",
"labels": [
"fix",
"Fix",
"Kind/Bug",
"Kind/Security"
]
},
{
"title": "## 🧪 Upgrade",
"labels": ["upgrade","Upgrade","Clean"]
}
,
{
"title": "## 📝 Documentation",
"labels": ["docs","Docs", "Kind/Documentation"]
},
{
"title": "## 🛠️ Maintenance",
"labels": [
"maintenance",
"Maintenance",
"Kind/Maintenance",
"chore",
"Chore",
"Kind/Chore"
]
},
{
"title": "## ⏪ Reverts",
"labels": [
"revert",
"Revert",
"Kind/Revert",
"Kind/Reverts",
"reverts",
"Reverts"
]
},
{
"title": "## 🗑️ Deprecation",
"labels": ["deprecation","Deprecation", "Kind/Deprecation"]
},
{
"title": "## ⚡️ Performance Improvements",
"labels": [
"perf",
"Perf",
"Kind/Performance"
]
},
{
"title": "## 🎨 Styling",
"labels": [
"style",
"Style",
"Kind/Style"
]
},
{
"title": "## 🎯 Other Changes",
"labels": []
}
],
"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,10 +1,6 @@
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
@@ -29,6 +25,8 @@ jobs:
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v5
- name: Set up Python - name: Set up Python
run: uv python install 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"
@@ -48,12 +46,15 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }} branch: ${{ github.ref }}
- name: Add release notes to environment - name: Build Changelog
id: add_release_notes id: build_changelog
run: | uses: https://github.com/mikepenz/release-changelog-builder-action@v5
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV with:
echo "${{ github.event.inputs.release_notes }}" >> $GITHUB_ENV platform: "gitea"
echo "EOF" >> $GITHUB_ENV baseURL: "http://192.168.178.110:3000"
configuration: ".gitea/changelog-config.json"
env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Build package - name: Build package
run: uv build run: uv build
- name: Publish package - name: Publish package
@@ -67,9 +68,9 @@ jobs:
if: ${{ github.event.inputs.github_release == 'true' }} if: ${{ github.event.inputs.github_release == 'true' }}
uses: softprops/action-gh-release@master uses: softprops/action-gh-release@master
with: with:
tag_name: ${{ env.VERSION }} tag_name: v${{ env.VERSION }}
release_name: Release ${{ env.VERSION }} release_name: Release ${{ env.VERSION }}
body: ${{ inputs.release_notes }} body: ${{steps.build_changelog.outputs.changelog}}
draft: false draft: false
prerelease: false prerelease: false
make_latest: true make_latest: true

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

9
main.py Normal file
View File

@@ -0,0 +1,9 @@
from src.komconfig import KomConfig
def test_komconfig():
print(KomConfig().general.log_file)
if __name__ == "__main__":
test_komconfig()

View File

@@ -1,15 +1,46 @@
[project] [project]
name = "komconfig" name = "komconfig"
version = "0.1.9" version = "0.2.5"
description = "A small library providing a config class that provides settings data for the KomSuite" description = "A small library providing a config class that provides settings data for the KomSuite"
readme = "README.md" readme = "README.md"
authors = [{ name = "WorldTeacher", email = "coding_contact@pm.me" }] authors = [{ name = "WorldTeacher", email = "coding_contact@pm.me" }]
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = ["omegaconf>=2.3.0"] dependencies = [
"appdirs>=1.4.4",
"omegaconf>=2.3.0",
]
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[dependency-groups] [dependency-groups]
test = ["pytest>=8.3.4"] dev = [
"pip>=25.1.1",
]
test = [
"pytest>=8.3.4",
"pytest-cov>=6.1.1",
]
[tool.bumpversion]
current_version = "0.2.5"
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 = []

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@@ -1,2 +1,38 @@
CONFIG_PATH = "~/.config/KomSuite/" __all__ = ["KomConfig", "app"]
from appdirs import AppDirs
app = AppDirs("KomSuite", "KomConfig")
import argparse
from .config import Settings as KomConfig from .config import Settings as KomConfig
def main():
parser = argparse.ArgumentParser(description="Retrieve configuration values.")
parser.add_argument("key", type=str, help="The configuration key to retrieve.")
args = parser.parse_args()
config = KomConfig()
try:
config_dict = config.dict()
if isinstance(config_dict, dict):
value = config_dict.get(args.key, None)
if value is None:
print(f"Key '{args.key}' not found in the configuration.")
else:
try:
print(str(value))
except Exception:
print(repr(value))
else:
print("Configuration data is not in dictionary format.")
except Exception as e:
print(f"Error retrieving key '{args.key}': {e}")
if __name__ == "__main__":
main()

30
src/komconfig/__main__.py Normal file
View File

@@ -0,0 +1,30 @@
import argparse
from src.komconfig import KomConfig
def main():
parser = argparse.ArgumentParser(description="Retrieve configuration values.")
parser.add_argument("key", type=str, help="The configuration key to retrieve.")
args = parser.parse_args()
config = KomConfig()
try:
config_dict = config.dict()
if isinstance(config_dict, dict):
value = config_dict.get(args.key, None)
if value is None:
print(f"Key '{args.key}' not found in the configuration.")
else:
try:
print(str(value))
except Exception:
print(repr(value))
else:
print("Configuration data is not in dictionary format.")
except Exception as e:
print(f"Error retrieving key '{args.key}': {e}")
if __name__ == "__main__":
main()

View File

@@ -1,11 +1,69 @@
from dataclasses import dataclass
from typing import List, Optional
import os import os
from omegaconf import OmegaConf, DictConfig from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from komconfig import CONFIG_PATH from typing import List, Optional, Union
from urllib.parse import quote_plus
SETTINGS_PATH = os.path.join(CONFIG_PATH, "config.yaml") from omegaconf import DictConfig, OmegaConf
from komconfig import app
SETTINGS_PATH = os.path.join(app.user_config_dir, "config.yaml")
@dataclass
class RemoteSettings:
url: str
port: int
user: str
password: str
database_name: str = "komcache"
@dataclass
class Cache:
mode: str
remote: dict[str, Union[str, int]] | RemoteSettings
local_path: str | Path
def __post__init__(self):
if self.mode == "remote":
self.remote = RemoteSettings(**self.remote)
else:
self.remote = None
self.local_path = Path(self.local_path).expanduser()
@property
def path(self):
if self.mode == "local":
return Path(self.local_path).expanduser()
else:
return f"{self.remote.url}:{self.remote.port}"
@property
def url(self):
password = quote_plus(self.remote.password) if self.remote else ""
return f"mysql+pymysql://{self.remote.user}:{password}@{self.remote.url}:{self.remote.port}/{self.remote.database_name}"
@dataclass
class Library:
"""Komga library settings."""
id: Optional[str] = None
name: Optional[str] = None
type: Optional[str] = None
media_path: Optional[str] = None
valid_extensions: Optional[List[str]] = None
def from_ddict(self, d: dict):
keys = d.keys()
for key in keys:
self.name = key
data = d[self.name]
for key, value in data.items():
setattr(self, key, value)
return self
@dataclass @dataclass
@@ -15,9 +73,9 @@ class Komga:
url: str url: str
user: str user: str
password: str password: str
media_path: str media_path: Path
api_key: str = None api_key: str = None
libraries: dict[str, str] = None libraries: List[Library] = None
def getattr(self, name): def getattr(self, name):
return getattr(self, name) return getattr(self, name)
@@ -27,7 +85,24 @@ class Komga:
def __post_init__(self): def __post_init__(self):
if "~" in self.media_path: if "~" in self.media_path:
self.media_path = os.path.expanduser(self.media_path) self.media_path = Path(os.path.expanduser(self.media_path))
if self.libraries:
self.libraries = [Library().from_ddict(lib) for lib in self.libraries]
def getLibraryByName(self, name: str) -> Optional[Library]:
"""Get a library by its name.
Args:
name (str): The name of the library.
Returns:
Optional[Library]: The library object if found, None otherwise.
"""
if self.libraries:
for lib in self.libraries:
if lib.name == name:
return lib
return None
@dataclass @dataclass
@@ -61,6 +136,23 @@ class Aria2:
setattr(self, name, value) setattr(self, name, value)
@dataclass
class QbitTorrent:
"""QbitTorrent settings."""
host: str
port: int
username: str
password: str
category: str
def getattr(self, name):
return getattr(self, name)
def _setattr(self, name, value):
setattr(self, name, value)
@dataclass @dataclass
class EbookSettings: class EbookSettings:
min_filesize: int min_filesize: int
@@ -133,22 +225,35 @@ class KomGrabber:
""" """
download_location: Path download_location: Path
tag_location: Path
copy_location: Optional[Path]
copy_files: Optional[List[str]]
copy: bool
get_chapters: bool get_chapters: bool
skip_parameters: List[str] skip_parameters: List[str]
aria2: Aria2
tag_interactive: bool tag_interactive: bool
ebook: EbookSettings ebook: EbookSettings
manga: ComicSettings manga: ComicSettings
check_interval: int check_interval: int
use_cache: bool use_cache: bool
cache_check_interval: int cache_check_interval: int
downloader: str
downloader_settings: Union[Aria2, QbitTorrent]
def __post_init__(self): def __post_init__(self):
self.skip_parameters = [param.lower() for param in self.skip_parameters] self.skip_parameters = [param.lower() for param in self.skip_parameters]
if "~" in self.download_location: # if "~" in self.download_location:
self.download_location = os.path.expanduser(self.download_location) # self.download_location = os.path.expanduser(self.download_location)
if isinstance(self.download_location, str): if isinstance(self.download_location, str):
self.download_location = Path(self.download_location) self.download_location = Path(self.download_location).expanduser()
if isinstance(self.tag_location, str):
self.tag_location = Path(self.tag_location).expanduser()
if isinstance(self.downloader_settings, dict):
if self.downloader == "aria2":
self.downloader_settings = Aria2(**self.downloader_settings)
elif self.downloader == "qbit":
self.downloader_settings = QbitTorrent(**self.downloader_settings)
def getattr(self, name): def getattr(self, name):
return getattr(self, name) return getattr(self, name)
@@ -161,9 +266,9 @@ class KomGrabber:
class KomTagger: class KomTagger:
"""KomTagger settings.""" """KomTagger settings."""
failed_location: str
success_location: str
sanitize_description: bool sanitize_description: bool
check_cache: bool
check_interval: int
def getattr(self, name): def getattr(self, name):
return getattr(self, name) return getattr(self, name)
@@ -172,18 +277,15 @@ class KomTagger:
setattr(self, name, value) setattr(self, name, value)
def __post_init__(self): def __post_init__(self):
if "~" in self.failed_location: pass
self.failed_location = os.path.expanduser(self.failed_location)
if "~" in self.success_location:
self.success_location = os.path.expanduser(self.success_location)
@dataclass @dataclass
class ComicVine: class ComicVine:
"""ComicVine settings.""" """ComicVine settings."""
api_key: str
url: str url: str
api_key: str
def getattr(self, name): def getattr(self, name):
return getattr(self, name) return getattr(self, name)
@@ -232,17 +334,11 @@ class API:
mangadex: MangaDex mangadex: MangaDex
comicsorg: ComicsOrg comicsorg: ComicsOrg
@property def __post_init__(self):
def comicvine(self): # Convert dictionaries to their respective dataclass objects
return self.comicvine self.comicvine = ComicVine(**self.comicvine)
self.mangadex = MangaDex(**self.mangadex)
@property self.comicsorg = ComicsOrg(**self.comicsorg)
def mangadex(self):
return self.mangadex
@property
def comicsorg(self):
return self.comicsorg
def getattr(self, name): def getattr(self, name):
return getattr(self, name) return getattr(self, name)
@@ -329,6 +425,10 @@ class Settings:
def komtagger(self): def komtagger(self):
return KomTagger(**self._config.komtagger) return KomTagger(**self._config.komtagger)
@property
def cache(self):
return Cache(**self._config.cache)
def komtagger_attr(self, name): def komtagger_attr(self, name):
return getattr(self.komtagger, name) return getattr(self.komtagger, name)

6
test.py Normal file
View File

@@ -0,0 +1,6 @@
from src.komconfig import KomConfig
cfg = KomConfig()
print(cfg.komga.libraries)

40
tests/test___init__.py Normal file
View File

@@ -0,0 +1,40 @@
import pytest
from unittest.mock import patch, MagicMock
from src.komconfig.__init__ import main
import sys
def test_valid_key():
mock_config = MagicMock()
mock_config.dict.return_value = {"valid_key": "value"}
with patch("src.komconfig.__init__.KomConfig", return_value=mock_config):
with patch("sys.argv", ["__init__.py", "valid_key"]):
main()
def test_key_not_found():
mock_config = MagicMock()
mock_config.dict.return_value = {"another_key": "value"}
with patch("src.komconfig.__init__.KomConfig", return_value=mock_config):
with patch("sys.argv", ["__init__.py", "missing_key"]):
main()
def test_non_dict_config():
mock_config = MagicMock()
mock_config.dict.return_value = None
with patch("src.komconfig.__init__.KomConfig", return_value=mock_config):
with patch("sys.argv", ["__init__.py", "any_key"]):
main()
def test_exception_handling():
mock_config = MagicMock()
mock_config.dict.side_effect = Exception("Test Exception")
with patch("src.komconfig.__init__.KomConfig", return_value=mock_config):
with patch("sys.argv", ["__init__.py", "any_key"]):
main()

18
tests/test_config.py Normal file
View File

@@ -0,0 +1,18 @@
from komconfig import KomConfig
import pytest
@pytest.fixture
def komconfig():
return KomConfig()
def test_komconfig(komconfig):
assert komconfig is not None
def test_general(komconfig):
assert KomConfig().general.log_file == "/var/log/komgrabber.log"
assert KomConfig().general.log_level == "INFO"