feat(global): create structure, implement parsers and search

This commit is contained in:
h
2025-11-07 00:41:30 +01:00
commit 5f228016eb
22 changed files with 984 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
# Development and testing
.idea
t

19
.pre-commit-config.yaml Normal file
View File

@@ -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

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

14
LICENSE-COMMERCIAL.md Normal file
View File

@@ -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

12
LICENSE-THIRD-PARTY.md Normal file
View File

@@ -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

36
LICENSE.md Normal file
View File

@@ -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 <https://www.gnu.org/licenses/>.
## 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.

34
README.md Normal file
View File

@@ -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

56
pyproject.toml Normal file
View File

@@ -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"]

View File

@@ -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",
]

View File

@@ -0,0 +1,4 @@
from .audiocomponent import AudioComponent, AudioUnitType
from .component import Component
__all__ = ["AudioComponent", "AudioUnitType", "Component"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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

View File

@@ -0,0 +1,4 @@
from .logic import Logic
from .plugins import Plugins, SearchResult
__all__ = ["Logic", "Plugins", "SearchResult"]

View File

@@ -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"]

View File

@@ -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"]

View File

View File

@@ -0,0 +1,4 @@
from .musicapps import MusicApps, Properties, Tagpool
from .tagset import Tagset
__all__ = ["MusicApps", "Properties", "Tagpool", "Tagset"]

View File

@@ -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"]

View File

@@ -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"]

124
uv.lock generated Normal file
View File

@@ -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" },
]