From 5f228016eb00d217e463d9964eb40ec79cfc33a8 Mon Sep 17 00:00:00 2001 From: h Date: Fri, 7 Nov 2025 00:41:30 +0100 Subject: [PATCH] feat(global): create structure, implement parsers and search --- .gitignore | 14 + .pre-commit-config.yaml | 19 ++ .python-version | 1 + LICENSE-COMMERCIAL.md | 14 + LICENSE-THIRD-PARTY.md | 12 + LICENSE.md | 36 +++ README.md | 34 +++ pyproject.toml | 56 ++++ src/logic_plugin_manager/__init__.py | 20 ++ .../components/__init__.py | 4 + .../components/audiocomponent.py | 107 ++++++++ .../components/component.py | 76 ++++++ src/logic_plugin_manager/defaults.py | 7 + src/logic_plugin_manager/exceptions.py | 34 +++ src/logic_plugin_manager/logic/__init__.py | 4 + src/logic_plugin_manager/logic/logic.py | 54 ++++ src/logic_plugin_manager/logic/plugins.py | 248 ++++++++++++++++++ src/logic_plugin_manager/py.typed | 0 src/logic_plugin_manager/tags/__init__.py | 4 + src/logic_plugin_manager/tags/musicapps.py | 73 ++++++ src/logic_plugin_manager/tags/tagset.py | 43 +++ uv.lock | 124 +++++++++ 22 files changed, 984 insertions(+) create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 LICENSE-COMMERCIAL.md create mode 100644 LICENSE-THIRD-PARTY.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/logic_plugin_manager/__init__.py create mode 100644 src/logic_plugin_manager/components/__init__.py create mode 100644 src/logic_plugin_manager/components/audiocomponent.py create mode 100644 src/logic_plugin_manager/components/component.py create mode 100644 src/logic_plugin_manager/defaults.py create mode 100644 src/logic_plugin_manager/exceptions.py create mode 100644 src/logic_plugin_manager/logic/__init__.py create mode 100644 src/logic_plugin_manager/logic/logic.py create mode 100644 src/logic_plugin_manager/logic/plugins.py create mode 100644 src/logic_plugin_manager/py.typed create mode 100644 src/logic_plugin_manager/tags/__init__.py create mode 100644 src/logic_plugin_manager/tags/musicapps.py create mode 100644 src/logic_plugin_manager/tags/tagset.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfcc61e --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Development and testing +.idea +t diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ddfa372 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.2 + hooks: + - id: ruff-check + args: [ --fix ] + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.9.0 + hooks: + - id: black + language_version: python3.13 + + - repo: https://github.com/pycqa/isort + rev: 7.0.0 + hooks: + - id: isort + name: isort (python) + language_version: python3.13 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/LICENSE-COMMERCIAL.md b/LICENSE-COMMERCIAL.md new file mode 100644 index 0000000..8f22aba --- /dev/null +++ b/LICENSE-COMMERCIAL.md @@ -0,0 +1,14 @@ +# Commercial License + +To use logic-plugin-manager in closed-source or commercial applications +without releasing your source code under AGPL-3.0, you need a commercial license. + +## What you get: + +- Use in proprietary/closed-source software +- No obligation to disclose your source code +- No AGPL "viral" requirements +- Priority support (optional) +- Custom features/integrations (optional) + +For questions and purchasing, contact: h@kotikot.com diff --git a/LICENSE-THIRD-PARTY.md b/LICENSE-THIRD-PARTY.md new file mode 100644 index 0000000..353784b --- /dev/null +++ b/LICENSE-THIRD-PARTY.md @@ -0,0 +1,12 @@ +# Third-Party Licenses + +This library uses the following third-party packages: + +--- + +## rapidfuzz + +**Version:** >=3.14.3 +**License:** MIT License +**Copyright:** Copyright © 2020-present Max Bachmann, Copyright © 2011 Adam Cohen +**Repository:** https://github.com/maxbachmann/RapidFuzz diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ac1aeae --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,36 @@ +# License + +This project is dual-licensed under the GNU Affero General Public License v3.0 +(AGPL-3.0) and a Commercial License. + +## For Open Source Projects (AGPL-3.0) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +## For Commercial Projects + +If you want to use this library in a proprietary/commercial application +without complying with the AGPL-3.0 terms (i.e., without releasing your +source code), you must obtain a commercial license. + +See [LICENSE-COMMERCIAL.md](LICENSE-COMMERCIAL.md) for details. + +**Contact:** h@kotikot.com + +--- + +## Third-Party Licenses + +This library depends on third-party packages with their own licenses. +See [LICENSE-THIRD-PARTY.md](LICENSE-THIRD-PARTY.md) for details. diff --git a/README.md b/README.md new file mode 100644 index 0000000..80ca425 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Logic Plugin Manager + +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE.md) +[![Commercial License](https://img.shields.io/badge/License-Commercial-green.svg)](LICENSE-COMMERCIAL.md) +[![PyPI version](https://badge.fury.io/py/logic-plugin-manager.svg)](https://pypi.org/project/logic-plugin-manager/) + +A utility for parsing and managing plugins in Logic Pro. + +## 📦 Installation + +```bash +uv add logic-plugin-manager +``` + +### With search functionality (includes rapidfuzz): +```bash +uv add logic-plugin-manager[search] +``` + +## 🚀 Quick Start +```python +from logic_plugin_manager import Logic + +logic = Logic() +``` + +## 📄 License +This project is dual-licensed: +#### 🆓 Open Source (AGPL-3.0) +Free for open source projects. See [LICENSE.md](LICENSE.md). +#### 💼 Commercial License +For closed-source/commercial use. See [COMMERCIAL.md](LICENSE-COMMERCIAL.md). + +Contact: h@kotikot.com diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7d62036 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[project] +name = "logic-plugin-manager" +version = "0.0.1" +description = "A utility for parsing and managing plugins in Logic Pro" +readme = "README.md" +authors = [ + { name = "h", email = "h@kotikot.com" } +] +requires-python = ">=3.13" +dependencies = [] +license = { file = "LICENSE.md" } +classifiers = [ + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "License :: Other/Proprietary License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", + "Operating System :: MacOS", + "Typing :: Typed", +] +keywords = [ + "logic pro", + "audio plugins", + "au", + "plugin manager", + "music production", +] + +[project.urls] +Homepage = "https://git.kotikot.com/lib/logic-plugin-manager" +Repository = "https://github.com/kotikotprojects/logic-plugin-manager" +"Commercial License" = "https://github.com/kotikotprojects/logic-plugin-manager/blob/main/LICENSE-COMMERCIAL.md" + +[project.optional-dependencies] +search = [ + "rapidfuzz>=3.14.3", +] + +[build-system] +requires = ["uv_build>=0.9.7,<0.10.0"] +build-backend = "uv_build" + +[dependency-groups] +dev = [ + "rich>=14.2.0", + "rapidfuzz>=3.14.3", +] + +[tool.isort] +profile = "black" + +[tool.ruff.lint] +extend-select = ["RUF022"] diff --git a/src/logic_plugin_manager/__init__.py b/src/logic_plugin_manager/__init__.py new file mode 100644 index 0000000..c3487fa --- /dev/null +++ b/src/logic_plugin_manager/__init__.py @@ -0,0 +1,20 @@ +from .components import AudioComponent, AudioUnitType, Component +from .exceptions import MusicAppsLoadError, PluginLoadError, TagsetLoadError +from .logic import Logic, Plugins, SearchResult +from .tags import MusicApps, Properties, Tagpool, Tagset + +__all__ = [ + "AudioComponent", + "AudioUnitType", + "Component", + "Logic", + "MusicApps", + "MusicAppsLoadError", + "PluginLoadError", + "Plugins", + "Properties", + "SearchResult", + "Tagpool", + "Tagset", + "TagsetLoadError", +] diff --git a/src/logic_plugin_manager/components/__init__.py b/src/logic_plugin_manager/components/__init__.py new file mode 100644 index 0000000..ed9b849 --- /dev/null +++ b/src/logic_plugin_manager/components/__init__.py @@ -0,0 +1,4 @@ +from .audiocomponent import AudioComponent, AudioUnitType +from .component import Component + +__all__ = ["AudioComponent", "AudioUnitType", "Component"] diff --git a/src/logic_plugin_manager/components/audiocomponent.py b/src/logic_plugin_manager/components/audiocomponent.py new file mode 100644 index 0000000..175c0b6 --- /dev/null +++ b/src/logic_plugin_manager/components/audiocomponent.py @@ -0,0 +1,107 @@ +from dataclasses import dataclass +from enum import Enum +from pathlib import Path + +from .. import defaults +from ..exceptions import CannotParseComponentError +from ..tags import Tagset + + +class AudioUnitType(Enum): + AUFX = ("aufx", "Audio FX", "Effect") + AUMU = ("aumu", "Instrument", "Music Device") + AUMF = ("aumf", "MIDI-controlled Effects", "Music Effect") + AUMI = ("aumi", "MIDI FX", "MIDI Generator") + AUGN = ("augn", "Generator", "Generator") + + @property + def code(self) -> str: + return self.value[0] + + @property + def display_name(self) -> str: + return self.value[1] + + @property + def alt_name(self) -> str: + return self.value[2] + + @classmethod + def from_code(cls, code: str) -> "AudioUnitType | None": + code_lower = code.lower() + for unit_type in cls: + if unit_type.code == code_lower: + return unit_type + return None + + @classmethod + def search(cls, query: str) -> list["AudioUnitType"]: + query_lower = query.lower() + results = [] + for unit_type in cls: + if ( + query_lower in unit_type.code + or query_lower in unit_type.display_name.lower() + or query_lower in unit_type.alt_name.lower() + ): + results.append(unit_type) + return results + + +@dataclass +class AudioComponent: + full_name: str + manufacturer: str + name: str + manufacturer_code: str + description: str + factory_function: str + type_code: str + type_name: AudioUnitType + subtype_code: str + version: int + tags_id: str + tagset: Tagset + + def __init__( + self, data: dict, *, lazy: bool = False, tags_path: Path = defaults.tags_path + ): + self.tags_path = tags_path + self.lazy = lazy + + try: + self.full_name = data.get("name") + self.manufacturer = self.full_name.split(": ")[0] + self.name = self.full_name.split(": ")[-1] + self.manufacturer_code = data.get("manufacturer") + self.description = data.get("description") + self.factory_function = data.get("factoryFunction") + self.type_code = data.get("type") + self.type_name = AudioUnitType.from_code(self.type_code) + self.subtype_code = data.get("subtype") + self.version = int(data.get("version")) + self.tags_id = ( + f"{self.type_code.encode('ascii').hex()}-" + f"{self.subtype_code.encode('ascii').hex()}-" + f"{self.manufacturer_code.encode('ascii').hex()}" + ) + except Exception as e: + raise CannotParseComponentError(f"An error occurred while parsing: {e}") + + if not lazy: + self.load() + + def load(self) -> "AudioComponent": + self.tagset = Tagset(self.tags_path / self.tags_id, lazy=self.lazy) + return self + + def __eq__(self, other): + if not isinstance(other, AudioComponent): + return NotImplemented + return self.tags_id == other.tags_id + + def __hash__(self): + return hash(self.tags_id) + + +__all__ = ["AudioComponent", "AudioUnitType"] diff --git a/src/logic_plugin_manager/components/component.py b/src/logic_plugin_manager/components/component.py new file mode 100644 index 0000000..99eb397 --- /dev/null +++ b/src/logic_plugin_manager/components/component.py @@ -0,0 +1,76 @@ +import plistlib +from dataclasses import dataclass +from pathlib import Path + +from .. import defaults +from ..exceptions import ( + CannotParseComponentError, + CannotParsePlistError, + NonexistentPlistError, + OldComponentFormatError, +) +from .audiocomponent import AudioComponent + + +@dataclass +class Component: + name: str + bundle_id: str + short_version: str + version: str + audio_components: list[AudioComponent] + + def __init__( + self, path: Path, *, lazy: bool = False, tags_path: Path = defaults.tags_path + ): + self.path = path if path.suffix == ".component" else Path(f"{path}.component") + self.lazy = lazy + self.tags_path = tags_path + if not lazy: + self.load() + + def _parse_plist(self): + info_plist_path = self.path / "Contents" / "Info.plist" + if not info_plist_path.exists(): + raise NonexistentPlistError(f"Info.plist not found at {info_plist_path}") + + try: + with open(info_plist_path, "rb") as fp: + plist_data = plistlib.load(fp) + return plist_data + except Exception as e: + raise CannotParsePlistError(f"An error occurred: {e}") + + def load(self) -> "Component": + plist_data = self._parse_plist() + + try: + self.name = self.path.name.removesuffix(".component") + self.bundle_id = plist_data["CFBundleIdentifier"] + self.version = plist_data["CFBundleVersion"] + self.short_version = plist_data["CFBundleShortVersionString"] + except Exception as e: + raise CannotParsePlistError( + f"An error occurred while extracting: {e}" + ) from e + try: + self.audio_components = [ + AudioComponent(name, lazy=self.lazy, tags_path=self.tags_path) + for name in plist_data["AudioComponents"] + ] + except KeyError as e: + raise OldComponentFormatError( + "This component is in an old format and cannot be loaded" + ) from e + except Exception as e: + raise CannotParseComponentError( + "An error occurred while loading components" + ) from e + + return self + + def __hash__(self): + return hash(self.bundle_id) + + +__all__ = ["Component"] diff --git a/src/logic_plugin_manager/defaults.py b/src/logic_plugin_manager/defaults.py new file mode 100644 index 0000000..485e68b --- /dev/null +++ b/src/logic_plugin_manager/defaults.py @@ -0,0 +1,7 @@ +from pathlib import Path + +components_path: Path = Path("/Library/Audio/Plug-Ins/Components") +tags_path: Path = Path("~/Music/Audio Music Apps/Databases/Tags").expanduser() + + +__all__ = ["components_path", "tags_path"] diff --git a/src/logic_plugin_manager/exceptions.py b/src/logic_plugin_manager/exceptions.py new file mode 100644 index 0000000..1e93ce0 --- /dev/null +++ b/src/logic_plugin_manager/exceptions.py @@ -0,0 +1,34 @@ +class PluginLoadError(Exception): + pass + + +class NonexistentPlistError(PluginLoadError): + pass + + +class CannotParsePlistError(PluginLoadError): + pass + + +class CannotParseComponentError(PluginLoadError): + pass + + +class OldComponentFormatError(PluginLoadError): + pass + + +class TagsetLoadError(Exception): + pass + + +class NonexistentTagsetError(TagsetLoadError): + pass + + +class CannotParseTagsetError(TagsetLoadError): + pass + + +class MusicAppsLoadError(Exception): + pass diff --git a/src/logic_plugin_manager/logic/__init__.py b/src/logic_plugin_manager/logic/__init__.py new file mode 100644 index 0000000..82a379d --- /dev/null +++ b/src/logic_plugin_manager/logic/__init__.py @@ -0,0 +1,4 @@ +from .logic import Logic +from .plugins import Plugins, SearchResult + +__all__ = ["Logic", "Plugins", "SearchResult"] diff --git a/src/logic_plugin_manager/logic/logic.py b/src/logic_plugin_manager/logic/logic.py new file mode 100644 index 0000000..e8a762d --- /dev/null +++ b/src/logic_plugin_manager/logic/logic.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from pathlib import Path + +from .. import defaults +from ..components import Component +from ..tags import MusicApps +from .plugins import Plugins + + +@dataclass +class Logic: + musicapps: MusicApps + plugins: Plugins + components: set[Component] + components_path: Path = defaults.components_path + tags_path: Path = defaults.tags_path + + def __init__( + self, + *, + components_path: Path | str = None, + tags_path: Path | str = None, + lazy: bool = False, + ): + self.components_path = ( + Path(components_path) if components_path else defaults.components_path + ) + self.tags_path = Path(tags_path) if tags_path else defaults.tags_path + + self.tags_path = self.tags_path.expanduser() + self.components_path = self.components_path.expanduser() + + self.musicapps = MusicApps(tags_path=self.tags_path, lazy=lazy) + self.plugins = Plugins() + self.components = set() + + self.lazy = lazy + + if not lazy: + self.discover_plugins() + + def discover_plugins(self): + for component_path in self.components_path.glob("*.component"): + try: + component = Component(component_path, lazy=self.lazy) + self.components.add(component) + for plugin in component.audio_components: + self.plugins.add(plugin, lazy=self.lazy) + except Exception as e: + assert e + continue + + +__all__ = ["Logic"] diff --git a/src/logic_plugin_manager/logic/plugins.py b/src/logic_plugin_manager/logic/plugins.py new file mode 100644 index 0000000..60a1134 --- /dev/null +++ b/src/logic_plugin_manager/logic/plugins.py @@ -0,0 +1,248 @@ +from collections import defaultdict +from dataclasses import dataclass + +from ..components import AudioComponent, AudioUnitType + + +@dataclass +class SearchResult: + plugin: AudioComponent + score: float + match_field: str + + +class Plugins: + def __init__(self): + self._plugins: set[AudioComponent] = set() + + self._by_full_name: dict[str, AudioComponent] = {} + self._by_manufacturer: dict[str, set[AudioComponent]] = defaultdict(set) + self._by_name: dict[str, AudioComponent] = {} + self._by_manufacturer_code: dict[str, set[AudioComponent]] = defaultdict(set) + self._by_factory_function: dict[str, set[AudioComponent]] = defaultdict(set) + self._by_type_code: dict[str, set[AudioComponent]] = defaultdict(set) + self._by_subtype_code: dict[str, set[AudioComponent]] = defaultdict(set) + self._by_tags_id: dict[str, AudioComponent] = {} + self._by_category: dict[str | None, set[AudioComponent]] = defaultdict(set) + + def add(self, plugin: AudioComponent, *, lazy: bool = False) -> "Plugins": + self._plugins.add(plugin) + if not lazy: + self._index_plugin(plugin) + return self + + def _index_plugin(self, plugin: AudioComponent): + if plugin.lazy: + plugin.load() + plugin.tagset.load() + + self._by_full_name[plugin.full_name.lower()] = plugin + self._by_manufacturer[plugin.manufacturer.lower()].add(plugin) + self._by_name[plugin.name.lower()] = plugin + self._by_manufacturer_code[plugin.manufacturer_code.lower()].add(plugin) + self._by_factory_function[plugin.factory_function.lower()].add(plugin) + self._by_type_code[plugin.type_code.lower()].add(plugin) + self._by_subtype_code[plugin.subtype_code.lower()].add(plugin) + self._by_tags_id[plugin.tags_id.lower()] = plugin + for tag in plugin.tagset.tags.keys(): + self._by_category[tag.lower()].add(plugin) + if not plugin.tagset.tags.keys(): + self._by_category[None].add(plugin) + + def reindex_all(self): + self._by_full_name.clear() + self._by_manufacturer.clear() + self._by_name.clear() + self._by_manufacturer_code.clear() + self._by_factory_function.clear() + self._by_type_code.clear() + self._by_subtype_code.clear() + self._by_tags_id.clear() + self._by_category.clear() + + for plugin in self._plugins: + self._index_plugin(plugin) + + def all(self): + return self._plugins.copy() + + def get_by_full_name(self, full_name: str) -> AudioComponent | None: + return self._by_full_name.get(full_name.lower()) + + def get_by_manufacturer(self, manufacturer: str) -> set[AudioComponent]: + return self._by_manufacturer.get(manufacturer.lower(), set()) + + def get_by_name(self, name: str) -> AudioComponent | None: + return self._by_name.get(name.lower()) + + def get_by_manufacturer_code(self, manufacturer_code: str) -> set[AudioComponent]: + return self._by_manufacturer_code.get(manufacturer_code.lower(), set()) + + def get_by_factory_function(self, factory_function: str) -> set[AudioComponent]: + return self._by_factory_function.get(factory_function.lower(), set()) + + def get_by_type_code(self, type_code: str) -> set[AudioComponent]: + return self._by_type_code.get(type_code.lower(), set()) + + def get_by_subtype_code(self, subtype_code: str) -> set[AudioComponent]: + return self._by_subtype_code.get(subtype_code.lower(), set()) + + def get_by_tags_id(self, tags_id: str) -> AudioComponent | None: + return self._by_tags_id.get(tags_id.lower()) + + def get_by_category(self, category: str | None) -> set[AudioComponent]: + return self._by_category.get(category.lower(), set()) + + def search_simple(self, query: str) -> set[AudioComponent]: + return { + plugin + for plugin in self._plugins + if query.lower() in plugin.full_name.lower() + } + + def search( + self, + query: str, + *, + use_fuzzy: bool = True, + fuzzy_threshold: int = 80, + max_results: int | None = None, + ) -> list[SearchResult]: + if not query or not query.strip(): + return [] + + query = query.strip() + query_lower = query.lower() + + if use_fuzzy: + try: + from rapidfuzz import fuzz + except ImportError: + raise ImportError( + "Search requires rapidfuzz as additional dependency. " + "Please install extra -> logic-plugin-manager[search]" + ) + else: + fuzz = None + + results: dict[AudioComponent, SearchResult] = {} + + def add_result(plugin_: AudioComponent, score_: float, field: str): + if plugin_ in results: + if score > results[plugin_].score: + results[plugin_] = SearchResult(plugin_, score_, field) + else: + results[plugin_] = SearchResult(plugin, score_, field) + + exact_match = self._by_tags_id.get(query_lower) + if exact_match: + add_result(exact_match, 1000.0, "tags_id") + + for plugin in self._plugins: + name_lower = plugin.name.lower() + full_name_lower = plugin.full_name.lower() + + if query_lower in name_lower: + score = 900.0 if name_lower.startswith(query_lower) else 850.0 + add_result(plugin, score, "name") + elif query_lower in full_name_lower: + score = 800.0 if full_name_lower.startswith(query_lower) else 750.0 + add_result(plugin, score, "full_name") + elif use_fuzzy and len(query) >= 3: + ratio_name = fuzz.token_set_ratio(query_lower, name_lower) + ratio_full = fuzz.token_set_ratio(query_lower, full_name_lower) + + best_ratio = max(ratio_name, ratio_full) + + if best_ratio >= fuzzy_threshold: + query_tokens = set(query_lower.split()) + name_tokens = set(full_name_lower.split()) + all_tokens_present = query_tokens.issubset(name_tokens) + + base_score = 700.0 if all_tokens_present else 650.0 + score = base_score + (best_ratio / 100.0 * 50) + add_result(plugin, score, "name") + + for plugin in self._plugins: + manufacturer_lower = plugin.manufacturer.lower() + + if query_lower in manufacturer_lower: + score = 650.0 if manufacturer_lower.startswith(query_lower) else 620.0 + add_result(plugin, score, "manufacturer") + elif use_fuzzy and len(query) >= 3: + ratio = fuzz.token_set_ratio(query_lower, manufacturer_lower) + if ratio >= fuzzy_threshold: + score = 580.0 + (ratio / 100.0 * 40) + add_result(plugin, score, "manufacturer") + + for plugin in self._plugins: + if plugin.tagset and plugin.tagset.tags: + for tag in plugin.tagset.tags.keys(): + tag_lower = tag.lower() + + if query_lower in tag_lower: + score = 550.0 if tag_lower.startswith(query_lower) else 520.0 + add_result(plugin, score, "category") + break + elif use_fuzzy and len(query) >= 3: + ratio = fuzz.ratio(query_lower, tag_lower) + if ratio >= fuzzy_threshold: + score = 480.0 + (ratio / 100.0 * 40) # 480-520 + add_result(plugin, score, "category") + break + + type_plugins = self._by_type_code.get(query_lower, set()) + for plugin in type_plugins: + add_result(plugin, 450.0, "type_code") + + matching_types = AudioUnitType.search(query) + for audio_type in matching_types: + type_plugins = self._by_type_code.get(audio_type.code, set()) + for plugin in type_plugins: + if query_lower == audio_type.display_name.lower(): + score = 440.0 + else: + score = 420.0 + add_result(plugin, score, "type_name") + + if use_fuzzy and len(query) == 4: + for type_code, plugins in self._by_type_code.items(): + ratio = fuzz.ratio(query_lower, type_code) + if ratio >= fuzzy_threshold: + for plugin in plugins: + score = 380.0 + (ratio / 100.0 * 30) # 380-410 + add_result(plugin, score, "type_code") + + for plugin in self._plugins: + subtype_lower = plugin.subtype_code.lower() + + if query_lower in subtype_lower: + score = 350.0 if subtype_lower.startswith(query_lower) else 330.0 + add_result(plugin, score, "subtype_code") + elif use_fuzzy and len(query) >= 3 and len(subtype_lower) >= 3: + ratio = fuzz.ratio(query_lower, subtype_lower) + if ratio >= fuzzy_threshold: + score = 290.0 + (ratio / 100.0 * 30) # 290-320 + add_result(plugin, score, "subtype_code") + + for plugin in self._plugins: + mfr_code_lower = plugin.manufacturer_code.lower() + + if query_lower in mfr_code_lower: + score = 250.0 if mfr_code_lower.startswith(query_lower) else 230.0 + add_result(plugin, score, "manufacturer_code") + elif use_fuzzy and len(query) >= 3 and len(mfr_code_lower) >= 3: + ratio = fuzz.ratio(query_lower, mfr_code_lower) + if ratio >= fuzzy_threshold: + score = 190.0 + (ratio / 100.0 * 30) # 190-220 + add_result(plugin, score, "manufacturer_code") + + sorted_results = sorted(results.values(), key=lambda r: r.score, reverse=True) + + if max_results: + sorted_results = sorted_results[:max_results] + + return sorted_results + + +__all__ = ["Plugins", "SearchResult"] diff --git a/src/logic_plugin_manager/py.typed b/src/logic_plugin_manager/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/logic_plugin_manager/tags/__init__.py b/src/logic_plugin_manager/tags/__init__.py new file mode 100644 index 0000000..e216a38 --- /dev/null +++ b/src/logic_plugin_manager/tags/__init__.py @@ -0,0 +1,4 @@ +from .musicapps import MusicApps, Properties, Tagpool +from .tagset import Tagset + +__all__ = ["MusicApps", "Properties", "Tagpool", "Tagset"] diff --git a/src/logic_plugin_manager/tags/musicapps.py b/src/logic_plugin_manager/tags/musicapps.py new file mode 100644 index 0000000..5199934 --- /dev/null +++ b/src/logic_plugin_manager/tags/musicapps.py @@ -0,0 +1,73 @@ +import plistlib +from dataclasses import dataclass +from pathlib import Path + +from .. import defaults +from ..exceptions import MusicAppsLoadError + + +def _parse_plist(path: Path): + if not path.exists(): + raise MusicAppsLoadError(f"File not found at {path}") + try: + with open(path, "rb") as fp: + plist_data = plistlib.load(fp) + return plist_data + except Exception as e: + raise MusicAppsLoadError(f"An error occurred: {e}") + + +@dataclass +class Tagpool: + categories: dict[str, int] + + def __init__(self, tags_path: Path, *, lazy: bool = False): + self.path = tags_path / "MusicApps.tagpool" + self.lazy = lazy + + if not lazy: + self.load() + + def load(self) -> "Tagpool": + self.categories = _parse_plist(self.path) + return self + + +@dataclass +class Properties: + sorting: list[str] + user_sorted: bool + + def __init__(self, tags_path: Path, *, lazy: bool = False): + self.path = tags_path / "MusicApps.properties" + self.lazy = lazy + + if not lazy: + self.load() + + def load(self) -> "Properties": + properties_data = _parse_plist(self.path) + self.sorting = properties_data.get("sorting", []) + self.user_sorted = bool(properties_data.get("user_sorted", False)) + return self + + +@dataclass +class MusicApps: + tagpool: Tagpool + properties: Properties + + def __init__(self, tags_path: Path = defaults.tags_path, *, lazy: bool = False): + self.path = tags_path + self.lazy = lazy + + if not lazy: + self.load() + + def load(self) -> "MusicApps": + self.tagpool = Tagpool(self.path, lazy=self.lazy) + self.properties = Properties(self.path, lazy=self.lazy) + return self + + +__all__ = ["MusicApps", "Properties", "Tagpool"] diff --git a/src/logic_plugin_manager/tags/tagset.py b/src/logic_plugin_manager/tags/tagset.py new file mode 100644 index 0000000..28dbd96 --- /dev/null +++ b/src/logic_plugin_manager/tags/tagset.py @@ -0,0 +1,43 @@ +import plistlib +from dataclasses import dataclass +from pathlib import Path + +from ..exceptions import CannotParseTagsetError, NonexistentTagsetError + + +@dataclass +class Tagset: + tags_id: str + nickname: str + shortname: str + tags: dict[str, str] + + def __init__(self, path: Path, *, lazy: bool = False): + self.path = path.with_suffix(".tagset") + self.lazy = lazy + + if not lazy: + self.load() + + def _parse_plist(self): + if not self.path.exists(): + raise NonexistentTagsetError(f".tagset not found at {self.path}") + try: + with open(self.path, "rb") as fp: + plist_data = plistlib.load(fp) + return plist_data + except Exception as e: + raise CannotParseTagsetError(f"An error occurred: {e}") + + def load(self) -> "Tagset": + tagset_data = self._parse_plist() + + self.tags_id = self.path.name.removesuffix(".tagset") + self.nickname = tagset_data.get("nickname") + self.shortname = tagset_data.get("shortname") + self.tags = tagset_data.get("tags") or {} + + return self + + +__all__ = ["Tagset"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..47f9b09 --- /dev/null +++ b/uv.lock @@ -0,0 +1,124 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "logic-plugin-manager" +version = "0.1.0" +source = { editable = "." } + +[package.optional-dependencies] +search = [ + { name = "rapidfuzz" }, +] + +[package.dev-dependencies] +dev = [ + { name = "rapidfuzz" }, + { name = "rich" }, +] + +[package.metadata] +requires-dist = [{ name = "rapidfuzz", marker = "extra == 'search'", specifier = ">=3.14.3" }] +provides-extras = ["search"] + +[package.metadata.requires-dev] +dev = [ + { name = "rapidfuzz", specifier = ">=3.14.3" }, + { name = "rich", specifier = ">=14.2.0" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" }, + { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" }, + { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" }, + { url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" }, + { url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" }, + { url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" }, + { url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +]