From cf0cb1ab53aceeebf07087fd9e1d62312f1ce8fb Mon Sep 17 00:00:00 2001 From: Uyanide Date: Mon, 30 Mar 2026 18:04:53 +0200 Subject: [PATCH] feat: replace typer with cycplots & improve cli --- lrcfetch/cli.py | 309 +++++++++++++++++++++++++++-------------------- lrcfetch/core.py | 15 ++- pyproject.toml | 10 +- uv.lock | 118 ++++++++++++------ 4 files changed, 274 insertions(+), 178 deletions(-) diff --git a/lrcfetch/cli.py b/lrcfetch/cli.py index 62beba7..4f18f00 100644 --- a/lrcfetch/cli.py +++ b/lrcfetch/cli.py @@ -4,67 +4,86 @@ Date: 2026-03-26 02:04:39 Description: CLI interface """ -import typer +import sys import time -from typing import Optional -from loguru import logger import os +from typing import Annotated +import cyclopts +from loguru import logger from .config import enable_debug from .models import TrackMeta, CacheStatus from .mpris import get_current_track -from .core import LrcManager +from .core import LrcManager, FetcherMethodType -app = typer.Typer( + +app = cyclopts.App( help="LRCFetch — Fetch line-synced lyrics for your music player.", - add_completion=True, ) +app.register_install_completion_command() + +cache_app = cyclopts.App(name="cache", help="Manage the local SQLite cache.") +app.command(cache_app) manager = LrcManager() -# Global state set by the app callback -_player: Optional[str] = None +# Global state set by the meta launcher +_player: str | None = None -@app.callback() -def main( - debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug logging."), - player: Optional[str] = typer.Option( - None, - "--player", - "-p", - help="Target a specific MPRIS player using its DBus name or a portion thereof.", - ), +@app.meta.default +def launcher( + *tokens: Annotated[str, cyclopts.Parameter(show=False, allow_leading_hyphen=True)], + debug: Annotated[ + bool, + cyclopts.Parameter( + name=["--debug", "-d"], negative="", help="Enable debug logging." + ), + ] = False, + player: Annotated[ + str | None, + cyclopts.Parameter( + name=["--player", "-p"], + help="Target a specific MPRIS player using its DBus name or a portion thereof.", + ), + ] = None, ): global _player if debug: enable_debug() _player = player + app(tokens) # fetch -@app.command() +@app.command def fetch( - method: Optional[str] = typer.Option( - None, - "--method", - help="Force a specific source (local, spotify, lrclib, lrclib-search, netease).", - ), - no_cache: bool = typer.Option( - False, "--no-cache", help="Bypass the cache for this request." - ), - only_synced: bool = typer.Option( - False, "--only-synced", help="Only accept synced (timed) lyrics." - ), + *, + method: Annotated[ + FetcherMethodType | None, + cyclopts.Parameter(help="Force a specific source."), + ] = None, + no_cache: Annotated[ + bool, + cyclopts.Parameter( + name="--no-cache", negative="", help="Bypass the cache for this request." + ), + ] = False, + only_synced: Annotated[ + bool, + cyclopts.Parameter( + name="--only-synced", negative="", help="Only accept synced (timed) lyrics." + ), + ] = False, ): """Fetch and print lyrics for the currently playing track.""" track = get_current_track(_player) if not track: logger.error("No active playing track found.") - raise typer.Exit(1) + sys.exit(1) logger.info(f"Track: {track.display_name()}") @@ -72,11 +91,11 @@ def fetch( if not result or not result.lyrics: logger.error("No lyrics found.") - raise typer.Exit(1) + sys.exit(1) if only_synced and result.status != CacheStatus.SUCCESS_SYNCED: logger.error("Only unsynced lyrics available (--only-synced requested).") - raise typer.Exit(1) + sys.exit(1) print(result.lyrics) @@ -84,27 +103,41 @@ def fetch( # search -@app.command() +@app.command def search( - title: str = typer.Option(..., "--title", "-t", help="Track title."), - artist: Optional[str] = typer.Option(None, "--artist", "-a", help="Artist name."), - album: Optional[str] = typer.Option(None, "--album", help="Album name."), - trackid: Optional[str] = typer.Option(None, "--trackid", help="Spotify track ID."), - length: Optional[int] = typer.Option( - None, "--length", "-l", help="Track duration in milliseconds." - ), - url: Optional[str] = typer.Option( - None, "--url", help="Local file URL (file:///...)." - ), - method: Optional[str] = typer.Option( - None, "--method", help="Force a specific source." - ), - no_cache: bool = typer.Option( - False, "--no-cache", help="Bypass the cache for this request." - ), - only_synced: bool = typer.Option( - False, "--only-synced", help="Only accept synced (timed) lyrics." - ), + *, + title: Annotated[ + str, cyclopts.Parameter(name=["--title", "-t"], help="Track title.") + ], + artist: Annotated[ + str | None, cyclopts.Parameter(name=["--artist", "-a"], help="Artist name.") + ] = None, + album: Annotated[str | None, cyclopts.Parameter(help="Album name.")] = None, + trackid: Annotated[str | None, cyclopts.Parameter(help="Spotify track ID.")] = None, + length: Annotated[ + int | None, + cyclopts.Parameter( + name=["--length", "-l"], help="Track duration in milliseconds." + ), + ] = None, + url: Annotated[ + str | None, cyclopts.Parameter(help="Local file URL (file:///...).") + ] = None, + method: Annotated[ + FetcherMethodType | None, cyclopts.Parameter(help="Force a specific source.") + ] = None, + no_cache: Annotated[ + bool, + cyclopts.Parameter( + name="--no-cache", negative="", help="Bypass the cache for this request." + ), + ] = False, + only_synced: Annotated[ + bool, + cyclopts.Parameter( + name="--only-synced", negative="", help="Only accept synced (timed) lyrics." + ), + ] = False, ): """Search for lyrics by metadata (bypasses MPRIS).""" track = TrackMeta( @@ -122,11 +155,11 @@ def search( if not result or not result.lyrics: logger.error("No lyrics found.") - raise typer.Exit(1) + sys.exit(1) if only_synced and result.status != CacheStatus.SUCCESS_SYNCED: logger.error("Only unsynced lyrics available (--only-synced requested).") - raise typer.Exit(1) + sys.exit(1) print(result.lyrics) @@ -134,32 +167,39 @@ def search( # export -@app.command() +@app.command def export( - output: Optional[str] = typer.Option( - None, - "--output", - "-o", - help="Output file path (default: - .lrc).", - ), - method: Optional[str] = typer.Option( - None, "--method", help="Force a specific source." - ), - no_cache: bool = typer.Option(False, "--no-cache", help="Bypass cache."), - overwrite: bool = typer.Option( - False, "--overwrite", "-f", help="Overwrite existing file." - ), + *, + output: Annotated[ + str | None, + cyclopts.Parameter( + name=["--output", "-o"], + help="Output file path (default: <Artist> - <Title>.lrc).", + ), + ] = None, + method: Annotated[ + FetcherMethodType | None, cyclopts.Parameter(help="Force a specific source.") + ] = None, + no_cache: Annotated[ + bool, cyclopts.Parameter(name="--no-cache", negative="", help="Bypass cache.") + ] = False, + overwrite: Annotated[ + bool, + cyclopts.Parameter( + name=["--overwrite", "-f"], negative="", help="Overwrite existing file." + ), + ] = False, ): """Export lyrics of the current track to a .lrc file.""" track = get_current_track(_player) if not track: logger.error("No active playing track found.") - raise typer.Exit(1) + sys.exit(1) result = manager.fetch_for_track(track, force_method=method, bypass_cache=no_cache) if not result or not result.lyrics: logger.error("No lyrics available to export.") - raise typer.Exit(1) + sys.exit(1) # Build default output path if not output: @@ -176,7 +216,7 @@ def export( if os.path.exists(output) and not overwrite: logger.error(f"File exists: {output} (use -f to overwrite)") - raise typer.Exit(1) + sys.exit(1) try: with open(output, "w", encoding="utf-8") as f: @@ -184,69 +224,22 @@ def export( logger.info(f"Exported lyrics to {output}") except Exception as e: logger.error(f"Failed to write file: {e}") - raise typer.Exit(1) + sys.exit(1) -# cache +# cache subcommands -@app.command() -def cache( - clear: bool = typer.Option(False, "--clear", help="Clear the entire cache."), - clear_current: bool = typer.Option( - False, "--clear-current", help="Clear cache for the current track." - ), - prune: bool = typer.Option(False, "--prune", help="Remove expired entries."), - stats: bool = typer.Option(False, "--stats", help="Show cache statistics."), - query: bool = typer.Option( - False, "--query", "-q", help="Show detailed cache info for the current track." - ), - query_all: bool = typer.Option( - False, "--query-all", help="Dump all cache entries." - ), +@cache_app.command +def query( + *, + all: Annotated[ + bool, + cyclopts.Parameter(name="--all", negative="", help="Dump all cache entries."), + ] = False, ): - """Manage the local SQLite cache.""" - if clear: - manager.cache.clear_all() - return - - if clear_current: - track = get_current_track(_player) - if not track: - logger.error("No active playing track found.") - raise typer.Exit(1) - manager.cache.clear_track(track) - return - - if prune: - manager.cache.prune() - return - - if stats: - s = manager.cache.stats() - print("=== Cache Statistics ===") - print(f"Total entries : {s['total']}") - print(f"Active : {s['active']}") - print(f"Expired : {s['expired']}") - if s["by_status"]: - print("\nBy status:") - for status, count in s["by_status"].items(): - print(f" {status}: {count}") - if s["by_source"]: - print("\nBy source:") - for source, count in s["by_source"].items(): - print(f" {source}: {count}") - return - - if query: - track = get_current_track(_player) - if not track: - logger.error("No active playing track found.") - raise typer.Exit(1) - _print_track_cache(track) - return - - if query_all: + """Show cached entries for the current track.""" + if all: rows = manager.cache.query_all() if not rows: print("Cache is empty.") @@ -256,10 +249,58 @@ def cache( print() return - logger.info( - "No action specified. Try --stats, --query, --query-all, " - "--prune, --clear, or --clear-current." - ) + track = get_current_track(_player) + if not track: + logger.error("No active playing track found.") + sys.exit(1) + _print_track_cache(track) + + +@cache_app.command +def clear( + *, + all: Annotated[ + bool, + cyclopts.Parameter(name="--all", negative="", help="Clear the entire cache."), + ] = False, +): + """Clear cached entries for the current track.""" + if all: + manager.cache.clear_all() + return + + track = get_current_track(_player) + if not track: + logger.error("No active playing track found.") + sys.exit(1) + manager.cache.clear_track(track) + + +@cache_app.command +def prune(): + """Remove expired cache entries.""" + manager.cache.prune() + + +@cache_app.command +def stats(): + """Show cache statistics.""" + s = manager.cache.stats() + print("=== Cache Statistics ===") + print(f"Total entries : {s['total']}") + print(f"Active : {s['active']}") + print(f"Expired : {s['expired']}") + if s["by_status"]: + print("\nBy status:") + for status, count in s["by_status"].items(): + print(f" {status}: {count}") + if s["by_source"]: + print("\nBy source:") + for source, count in s["by_source"].items(): + print(f" {source}: {count}") + + +# helpers def _print_track_cache(track: TrackMeta) -> None: @@ -317,7 +358,7 @@ def _print_cache_row(row: dict, indent: str = "") -> None: def run(): - app() + app.meta() if __name__ == "__main__": diff --git a/lrcfetch/core.py b/lrcfetch/core.py index 3edfabe..c380cbf 100644 --- a/lrcfetch/core.py +++ b/lrcfetch/core.py @@ -14,6 +14,7 @@ Fetch pipeline: from typing import Optional from loguru import logger +from typing import Literal from .fetchers.netease import NeteaseFetcher from .fetchers.lrclib_search import LrclibSearchFetcher @@ -27,6 +28,11 @@ from .lrc import LRC_LINE_RE, normalize_tags from .config import TTL_SYNCED, TTL_UNSYNCED, TTL_NOT_FOUND, TTL_NETWORK_ERROR from .models import TrackMeta, LyricResult, CacheStatus +METHODS = ("local", "cache-search", "spotify", "lrclib", "lrclib-search", "netease") +FetcherMethodType = Literal[ + "local", "cache-search", "spotify", "lrclib", "lrclib-search", "netease" +] + def _normalize_unsynced(lyrics: str) -> str: """Normalize unsynced lyrics so every line has a [00:00.00] tag. @@ -65,7 +71,7 @@ class LrcManager: def __init__(self) -> None: self.cache = CacheEngine() - self.fetchers: dict[str, BaseFetcher] = { + self.fetchers: dict[FetcherMethodType, BaseFetcher] = { "local": LocalFetcher(), "cache-search": CacheSearchFetcher(self.cache), "spotify": SpotifyFetcher(), @@ -73,9 +79,12 @@ class LrcManager: "lrclib-search": LrclibSearchFetcher(), "netease": NeteaseFetcher(), } + assert set(self.fetchers) == set(METHODS), ( + f"METHODS and fetchers out of sync: {set(METHODS) ^ set(self.fetchers)}" + ) def _build_sequence( - self, track: TrackMeta, force_method: Optional[str] = None + self, track: TrackMeta, force_method: Optional[FetcherMethodType] = None ) -> list[BaseFetcher]: """Determine the ordered list of fetchers to try.""" if force_method: @@ -103,7 +112,7 @@ class LrcManager: def fetch_for_track( self, track: TrackMeta, - force_method: Optional[str] = None, + force_method: Optional[FetcherMethodType] = None, bypass_cache: bool = False, ) -> Optional[LyricResult]: """Fetch lyrics for *track* using the fallback pipeline. diff --git a/pyproject.toml b/pyproject.toml index dd25c62..3eca0f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,12 @@ build-backend = "hatchling.build" [project] name = "lrcfetch" -version = "0.1.0" +version = "0.1.2" description = "Fetch line-synced lyrics for your music player." readme = "README.md" requires-python = ">=3.13" dependencies = [ + "cyclopts>=4.10.1", "dbus-next>=0.2.3", "httpx>=0.28.1", "loguru>=0.7.3", @@ -16,8 +17,13 @@ dependencies = [ "platformdirs>=4.9.4", "pydantic>=2.12.5", "python-dotenv>=1.2.2", - "typer>=0.24.1", ] [project.scripts] lrcfetch = "lrcfetch.cli:run" + +[tool.ruff.lint] +ignore = ["E402"] + +[dependency-groups] +dev = ["ruff>=0.15.8"] diff --git a/uv.lock b/uv.lock index 5b3b259..8f08976 100644 --- a/uv.lock +++ b/uv.lock @@ -2,15 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.13" -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -32,6 +23,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -41,18 +41,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -62,6 +50,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cyclopts" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/c4/2ce2ca1451487dc7d59f09334c3fa1182c46cfcf0a2d5f19f9b26d53ac74/cyclopts-4.10.1.tar.gz", hash = "sha256:ad4e4bb90576412d32276b14a76f55d43353753d16217f2c3cd5bdceba7f15a0", size = 166623, upload-time = "2026-03-23T14:43:01.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/2261922126b2e50c601fe22d7ff5194e0a4d50e654836260c0665e24d862/cyclopts-4.10.1-py3-none-any.whl", hash = "sha256:35f37257139380a386d9fe4475e1e7c87ca7795765ef4f31abba579fcfcb6ecd", size = 204331, upload-time = "2026-03-23T14:43:02.625Z" }, +] + [[package]] name = "dbus-next" version = "0.2.3" @@ -71,6 +74,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -132,9 +153,10 @@ wheels = [ [[package]] name = "lrcfetch" -version = "0.1.0" +version = "0.1.2" source = { editable = "." } dependencies = [ + { name = "cyclopts" }, { name = "dbus-next" }, { name = "httpx" }, { name = "loguru" }, @@ -142,11 +164,16 @@ dependencies = [ { name = "platformdirs" }, { name = "pydantic" }, { name = "python-dotenv" }, - { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, ] [package.metadata] requires-dist = [ + { name = "cyclopts", specifier = ">=4.10.1" }, { name = "dbus-next", specifier = ">=0.2.3" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "loguru", specifier = ">=0.7.3" }, @@ -154,11 +181,10 @@ requires-dist = [ { name = "platformdirs", specifier = ">=4.9.4" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "python-dotenv", specifier = ">=1.2.2" }, - { name = "typer", specifier = ">=0.24.1" }, ] [package.metadata.requires-dev] -dev = [] +dev = [{ name = "ruff", specifier = ">=0.15.8" }] [[package]] name = "markdown-it-py" @@ -299,27 +325,41 @@ wheels = [ ] [[package]] -name = "shellingham" -version = "1.5.4" +name = "rich-rst" +version = "1.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, ] [[package]] -name = "typer" -version = "0.24.1" +name = "ruff" +version = "0.15.8" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, + { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, ] [[package]]