diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b242c91..b3f4e17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 + rev: v0.8.4 hooks: - id: ruff-format - id: ruff - repo: https://github.com/executablebooks/mdformat - rev: 0.7.19 + rev: 0.7.21 hooks: - id: mdformat additional_dependencies: diff --git a/README.md b/README.md index 5d7d785..c19faa4 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,6 @@ Unlike other tagging tools, Perdoo employs a manual approach when metadata files ## Services - [Comicvine](https://comicvine.gamespot.com) using the [Simyan](https://github.com/Metron-Project/Simyan) library. -- [League of Comic Geeks](https://leagueofcomicgeeks.com) using the [Himon](https://github.com/Buried-In-Code/Himon) library. - [Marvel](https://www.marvel.com/comics) using the [Esak](https://github.com/Metron-Project/Esak) library. - [Metron](https://metron.cloud) using the [Mokkari](https://github.com/Metron-Project/Mokkari) library. diff --git a/perdoo/__main__.py b/perdoo/__main__.py index b4c1c68..2f4125e 100644 --- a/perdoo/__main__.py +++ b/perdoo/__main__.py @@ -8,7 +8,7 @@ from comicfn2dict import comicfn2dict from typer import Argument, Context, Exit, Option, Typer -from perdoo import __version__, setup_logging +from perdoo import __version__, get_cache_root, setup_logging from perdoo.archives import CBRArchive, get_archive from perdoo.cli import archive_app, settings_app from perdoo.console import CONSOLE @@ -22,9 +22,16 @@ ) from perdoo.metadata import ComicInfo, MetronInfo, get_metadata from perdoo.metadata.metron_info import InformationSource -from perdoo.services import BaseService, Comicvine, League, Marvel, Metron +from perdoo.services import BaseService, Comicvine, Marvel, Metron from perdoo.settings import Service, Services, Settings -from perdoo.utils import IssueSearch, Search, SeriesSearch, delete_empty_folders, list_files +from perdoo.utils import ( + IssueSearch, + Search, + SeriesSearch, + delete_empty_folders, + list_files, + recursive_delete, +) app = Typer(help="CLI tool for managing comic collections and settings.") app.add_typer(archive_app, name="archive") @@ -66,8 +73,6 @@ def get_services(settings: Services) -> dict[Service, BaseService]: output = {} if settings.comicvine.api_key: output[Service.COMICVINE] = Comicvine(settings.comicvine) - if settings.league_of_comic_geeks.client_id and settings.league_of_comic_geeks.client_secret: - output[Service.LEAGUE_OF_COMIC_GEEKS] = League(settings.league_of_comic_geeks) if settings.marvel.public_key and settings.marvel.private_key: output[Service.MARVEL] = Marvel(settings.marvel) if settings.metron.username and settings.metron.password: @@ -89,7 +94,6 @@ def get_search_details( volume=metron_info.series.volume, year=metron_info.series.start_year, comicvine=series_id if source == InformationSource.COMIC_VINE else None, - league=series_id if source == InformationSource.LEAGUE_OF_COMIC_GEEKS else None, marvel=series_id if source == InformationSource.MARVEL else None, metron=series_id if source == InformationSource.METRON else None, ), @@ -101,14 +105,6 @@ def get_search_details( ), None, ), - league=next( - iter( - x.value - for x in metron_info.ids - if x.source == InformationSource.LEAGUE_OF_COMIC_GEEKS - ), - None, - ), marvel=next( iter(x.value for x in metron_info.ids if x.source == InformationSource.MARVEL), None, @@ -169,6 +165,16 @@ def run( bool, Option("--skip-organize", help="Skip organize/moving comics to appropriate directories."), ] = False, + clean_cache: Annotated[ + bool, + Option( + "--clean", + "-c", + show_default=False, + help="Clean the cache before starting the synchronization process. " + "Removes all cached files.", + ), + ] = False, debug: Annotated[ bool, Option("--debug", help="Enable debug mode to show extra information.") ] = False, @@ -188,8 +194,12 @@ def run( "flags.skip-clean": skip_clean, "flags.skip-rename": skip_rename, "flags.skip-organize": skip_organize, + "flags.clean-cache": clean_cache, } ) + if clean_cache: + LOGGER.info("Cleaning Cache") + recursive_delete(path=get_cache_root()) services = get_services(settings=settings.services) if not services and sync != SyncOption.SKIP: LOGGER.warning("No external services configured") diff --git a/perdoo/services/__init__.py b/perdoo/services/__init__.py index bb372c5..2b29fa7 100644 --- a/perdoo/services/__init__.py +++ b/perdoo/services/__init__.py @@ -1,7 +1,6 @@ -__all__ = ["BaseService", "Comicvine", "League", "Marvel", "Metron"] +__all__ = ["BaseService", "Comicvine", "Marvel", "Metron"] from perdoo.services._base import BaseService from perdoo.services.comicvine import Comicvine -from perdoo.services.league import League from perdoo.services.marvel import Marvel from perdoo.services.metron import Metron diff --git a/perdoo/services/comicvine.py b/perdoo/services/comicvine.py index f6e16e5..c0d4e28 100644 --- a/perdoo/services/comicvine.py +++ b/perdoo/services/comicvine.py @@ -163,22 +163,22 @@ def load_role(value: str) -> Role: return Role.OTHER return MetronInfo( - ids=[Id(primary=True, source=InformationSource.COMIC_VINE, value=issue.id)], - publisher=Publisher(id=series.publisher.id, name=series.publisher.name), - series=Series(id=series.id, name=series.name, start_year=series.start_year), + ids=[Id(primary=True, source=InformationSource.COMIC_VINE, value=str(issue.id))], + publisher=Publisher(id=str(series.publisher.id), name=series.publisher.name), + series=Series(id=str(series.id), name=series.name, start_year=series.start_year), collection_title=issue.name, number=issue.number, summary=issue.summary, cover_date=issue.cover_date, store_date=issue.store_date, - arcs=[Arc(id=x.id, name=x.name) for x in issue.story_arcs], - characters=[Resource[str](id=x.id, value=x.name) for x in issue.characters], - teams=[Resource[str](id=x.id, value=x.name) for x in issue.teams], - locations=[Resource[str](id=x.id, value=x.name) for x in issue.locations], + arcs=[Arc(id=str(x.id), name=x.name) for x in issue.story_arcs], + characters=[Resource[str](id=str(x.id), value=x.name) for x in issue.characters], + teams=[Resource[str](id=str(x.id), value=x.name) for x in issue.teams], + locations=[Resource[str](id=str(x.id), value=x.name) for x in issue.locations], urls=[Url(primary=True, value=issue.site_url)], credits=[ Credit( - creator=Resource[str](id=x.id, value=x.name), + creator=Resource[str](id=str(x.id), value=x.name), roles=[ Resource[Role](value=load_role(value=r.strip())) for r in re.split(r"[~\r\n,]+", x.roles) diff --git a/perdoo/services/league.py b/perdoo/services/league.py deleted file mode 100644 index 140cc11..0000000 --- a/perdoo/services/league.py +++ /dev/null @@ -1,51 +0,0 @@ -__all__ = ["League"] - -import logging - -from himon.league_of_comic_geeks import LeagueofComicGeeks as Himon -from himon.schemas.comic import Comic -from himon.schemas.series import Series -from himon.sqlite_cache import SQLiteCache - -from perdoo import get_cache_root -from perdoo.metadata import ComicInfo, MetronInfo -from perdoo.services._base import BaseService -from perdoo.settings import LeagueOfComicGeeks as LeagueSettings -from perdoo.utils import IssueSearch, Search, SeriesSearch - -LOGGER = logging.getLogger(__name__) - - -class League(BaseService[Series, Comic]): - def __init__(self, settings: LeagueSettings): - cache = SQLiteCache(path=get_cache_root() / "himon.sqlite", expiry=14) - self.session = Himon( - client_id=settings.client_id, - client_secret=settings.client_secret, - access_token=settings.access_token, - cache=cache, - ) - if not settings.access_token: - LOGGER.info("Generating new access token") - self.session.access_token = settings.access_token = self.session.generate_access_token() - - def _search_series(self, name: str | None, volume: int | None, year: int | None) -> int | None: - pass - - def fetch_series(self, search: SeriesSearch) -> Series | None: - pass - - def _search_issue(self, series_id: int, number: str | None) -> int | None: - pass - - def fetch_issue(self, series_id: int, search: IssueSearch) -> Comic | None: - pass - - def _process_metron_info(self, series: Series, issue: Comic) -> MetronInfo | None: - pass - - def _process_comic_info(self, series: Series, issue: Comic) -> ComicInfo | None: - pass - - def fetch(self, search: Search) -> tuple[MetronInfo | None, ComicInfo | None]: # noqa: ARG002 - return None, None diff --git a/perdoo/services/marvel.py b/perdoo/services/marvel.py index 3686df4..a64d821 100644 --- a/perdoo/services/marvel.py +++ b/perdoo/services/marvel.py @@ -161,29 +161,29 @@ def load_role(value: str) -> Role: return Role.OTHER return MetronInfo( - ids=[Id(primary=True, source=InformationSource.MARVEL, value=issue.id)], + ids=[Id(primary=True, source=InformationSource.MARVEL, value=str(issue.id))], publisher=Publisher(name="Marvel"), series=Series( - id=series.id, + id=str(series.id), name=series.title, format=load_format(value=issue.format), start_year=series.start_year, ), collection_title=issue.title, number=issue.issue_number, - stories=[Resource[str](id=x.id, value=x.name) for x in issue.stories], + stories=[Resource[str](id=str(x.id), value=x.name) for x in issue.stories], summary=issue.description, prices=[Price(country="US", value=issue.prices.print)] if issue.prices else [], store_date=issue.dates.on_sale, page_count=issue.page_count, - arcs=[Arc(id=x.id, name=x.name) for x in issue.events], - characters=[Resource[str](id=x.id, value=x.name) for x in issue.characters], + arcs=[Arc(id=str(x.id), name=x.name) for x in issue.events], + characters=[Resource[str](id=str(x.id), value=x.name) for x in issue.characters], gtin=GTIN(isbn=issue.isbn, upc=issue.upc) if issue.isbn and issue.upc else None, age_rating=load_age_rating(value=series.rating), urls=[Url(primary=True, value=issue.urls.detail)], credits=[ Credit( - creator=Resource[str](id=x.id, value=x.name), + creator=Resource[str](id=str(x.id), value=x.name), roles=[Resource[Role](value=load_role(value=x.role))], ) for x in issue.creators diff --git a/perdoo/services/metron.py b/perdoo/services/metron.py index ebb2eaf..5020673 100644 --- a/perdoo/services/metron.py +++ b/perdoo/services/metron.py @@ -188,20 +188,22 @@ def load_role(value: str) -> Role: except ValueError: return Role.OTHER - ids = [Id(primary=True, source=InformationSource.METRON, value=issue.id)] + ids = [Id(primary=True, source=InformationSource.METRON, value=str(issue.id))] if issue.cv_id: - ids.append(Id(source=InformationSource.COMIC_VINE, value=issue.cv_id)) + ids.append(Id(source=InformationSource.COMIC_VINE, value=str(issue.cv_id))) + if issue.gcd_id: + ids.append(Id(source=InformationSource.GRAND_COMICS_DATABASE, value=str(issue.gcd_id))) return MetronInfo( ids=ids, publisher=Publisher( - id=series.publisher.id, + id=str(series.publisher.id), name=series.publisher.name, - imprint=Resource[str](id=series.imprint.id, value=series.imprint.name) + imprint=Resource[str](id=str(series.imprint.id), value=series.imprint.name) if series.imprint else None, ), series=Series( - id=series.id, + id=str(series.id), name=series.name, sort_name=series.sort_name, volume=series.volume, @@ -216,21 +218,23 @@ def load_role(value: str) -> Role: cover_date=issue.cover_date, store_date=issue.store_date, page_count=issue.page_count or 0, - genres=[Resource[str](id=x.id, value=x.name) for x in issue.series.genres], - arcs=[Arc(id=x.id, name=x.name) for x in issue.arcs], - characters=[Resource[str](id=x.id, value=x.name) for x in issue.characters], - teams=[Resource[str](id=x.id, value=x.name) for x in issue.teams], - universes=[Universe(id=x.id, name=x.name) for x in issue.universes], + genres=[Resource[str](id=str(x.id), value=x.name) for x in issue.series.genres], + arcs=[Arc(id=str(x.id), name=x.name) for x in issue.arcs], + characters=[Resource[str](id=str(x.id), value=x.name) for x in issue.characters], + teams=[Resource[str](id=str(x.id), value=x.name) for x in issue.teams], + universes=[Universe(id=str(x.id), name=x.name) for x in issue.universes], gtin=GTIN(isbn=issue.isbn or None, upc=issue.upc or None) if issue.isbn or issue.upc else None, age_rating=AgeRating.load(value=issue.rating.name), - reprints=[Resource[str](id=x.id, value=x.issue) for x in issue.reprints], + reprints=[Resource[str](id=str(x.id), value=x.issue) for x in issue.reprints], urls=[Url(primary=True, value=issue.resource_url)], credits=[ Credit( - creator=Resource[str](id=x.id, value=x.creator), - roles=[Resource[Role](id=r.id, value=load_role(value=r.name)) for r in x.role], + creator=Resource[str](id=str(x.id), value=x.creator), + roles=[ + Resource[Role](id=str(r.id), value=load_role(value=r.name)) for r in x.role + ], ) for x in issue.credits ], diff --git a/perdoo/settings.py b/perdoo/settings.py index 15fde77..a34fea3 100644 --- a/perdoo/settings.py +++ b/perdoo/settings.py @@ -1,7 +1,6 @@ __all__ = [ "ComicInfo", "Comicvine", - "LeagueOfComicGeeks", "Marvel", "Metadata", "Metron", @@ -71,12 +70,6 @@ class Comicvine(SettingsModel): api_key: str | None = None -class LeagueOfComicGeeks(SettingsModel): - client_id: str | None = None - client_secret: str | None = None - access_token: str | None = None - - class Marvel(SettingsModel): public_key: str | None = None private_key: str | None = None @@ -89,7 +82,6 @@ class Metron(SettingsModel): class Service(Enum): COMICVINE = "Comicvine" - LEAGUE_OF_COMIC_GEEKS = "League of Comic Geeks" MARVEL = "Marvel" METRON = "Metron" @@ -106,7 +98,6 @@ def __str__(self) -> str: class Services(SettingsModel): comicvine: Comicvine = Comicvine() - league_of_comic_geeks: LeagueOfComicGeeks = LeagueOfComicGeeks() marvel: Marvel = Marvel() metron: Metron = Metron() order: tuple[Service, ...] = (Service.METRON, Service.MARVEL, Service.COMICVINE) diff --git a/perdoo/utils.py b/perdoo/utils.py index 9801684..60c5c49 100644 --- a/perdoo/utils.py +++ b/perdoo/utils.py @@ -25,7 +25,6 @@ class SeriesSearch: volume: int | None = None year: int | None = None comicvine: int | None = None - league: int | None = None marvel: int | None = None metron: int | None = None @@ -34,7 +33,6 @@ class SeriesSearch: class IssueSearch: number: str | None = None comicvine: int | None = None - league: int | None = None marvel: int | None = None metron: int | None = None @@ -80,6 +78,15 @@ def flatten_dict(content: dict[str, Any], parent_key: str = "") -> dict[str, Any return dict(humansorted(items.items(), alg=ns.NA | ns.G)) +def recursive_delete(path: Path) -> None: + for item in path.iterdir(): + if item.is_dir(): + recursive_delete(item) + else: + item.unlink() + path.rmdir() + + def delete_empty_folders(folder: Path) -> None: if folder.is_dir(): for subfolder in folder.iterdir(): diff --git a/pyproject.toml b/pyproject.toml index b1a3e2c..8302dd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,10 @@ dev = [ "pre-commit >= 4.0.1" ] tests = [ - "pytest >= 8.3.3", + "pytest >= 8.3.4", "pytest-cov >= 6.0.0", "tox >= 4.23.2", - "tox-uv >= 1.16.0" + "tox-uv >= 1.16.1" ] [project] @@ -38,17 +38,17 @@ dependencies = [ "esak >= 2.0.0", "himon >= 0.6.1", "lxml >= 5.3.0", - "mokkari >= 3.4.0", + "mokkari >= 3.5.0", "natsort >= 8.4.0", "pillow >= 11.0.0", - "pydantic >= 2.10.1", - "pydantic-xml >= 2.14.0", + "pydantic >= 2.10.4", + "pydantic-xml >= 2.14.1", "rarfile >= 4.2", - "rich >= 13.9.3", - "simyan >= 1.3.0", - "tomli >= 2.1.0 ; python_version < \"3.11\"", + "rich >= 13.9.4", + "simyan >= 1.4.0", + "tomli >= 2.2.1 ; python_version < '3.11'", "tomli-w >= 1.1.0", - "typer >= 0.13.1" + "typer >= 0.15.1" ] description = "Unify and organize your comic collection." dynamic = ["version"] @@ -71,12 +71,21 @@ Homepage = "https://pypi.org/project/Perdoo" Issues = "https://github.com/Buried-In-Code/Perdoo/issues" Source = "https://github.com/Buried-In-Code/Perdoo" +[tool.coverage.report] +show_missing = true + +[tool.coverage.run] +source = ["perdoo"] + [tool.hatch.build.targets.sdist] exclude = [".github/"] [tool.hatch.version] path = "perdoo/__init__.py" +[tool.pytest.ini_options] +addopts = ["--cov"] + [tool.ruff] extend-exclude = ["perdoo/archives/zipfile_remove.py"] fix = true @@ -96,7 +105,8 @@ ignore = [ "COM812", "D", "DTZ", - "EM", + "EM101", + "EM102", "FBT", "PLR0912", "PLR0913", @@ -132,3 +142,16 @@ convention = "google" [tool.ruff.lint.pyupgrade] keep-runtime-typing = true + +[tool.tomlsort] +all = true +overrides."tool.tox.env_list".inline_arrays = false +overrides."tool.tox.testenv.commands".inline_arrays = false + +[tool.tox] +env_list = ["3.10", "3.11", "3.12", "3.13"] +min_version = "4.22" + +[tool.tox.env_run_base] +commands = [["pytest"]] +dependency_groups = ["tests"]