feat(global): create structure, implement parsers and search
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal 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
19
.pre-commit-config.yaml
Normal 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
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
14
LICENSE-COMMERCIAL.md
Normal file
14
LICENSE-COMMERCIAL.md
Normal 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
12
LICENSE-THIRD-PARTY.md
Normal 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
36
LICENSE.md
Normal 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
34
README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Logic Plugin Manager
|
||||||
|
|
||||||
|
[](LICENSE.md)
|
||||||
|
[](LICENSE-COMMERCIAL.md)
|
||||||
|
[](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
56
pyproject.toml
Normal 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"]
|
||||||
20
src/logic_plugin_manager/__init__.py
Normal file
20
src/logic_plugin_manager/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
4
src/logic_plugin_manager/components/__init__.py
Normal file
4
src/logic_plugin_manager/components/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .audiocomponent import AudioComponent, AudioUnitType
|
||||||
|
from .component import Component
|
||||||
|
|
||||||
|
__all__ = ["AudioComponent", "AudioUnitType", "Component"]
|
||||||
107
src/logic_plugin_manager/components/audiocomponent.py
Normal file
107
src/logic_plugin_manager/components/audiocomponent.py
Normal 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"]
|
||||||
76
src/logic_plugin_manager/components/component.py
Normal file
76
src/logic_plugin_manager/components/component.py
Normal 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"]
|
||||||
7
src/logic_plugin_manager/defaults.py
Normal file
7
src/logic_plugin_manager/defaults.py
Normal 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"]
|
||||||
34
src/logic_plugin_manager/exceptions.py
Normal file
34
src/logic_plugin_manager/exceptions.py
Normal 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
|
||||||
4
src/logic_plugin_manager/logic/__init__.py
Normal file
4
src/logic_plugin_manager/logic/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .logic import Logic
|
||||||
|
from .plugins import Plugins, SearchResult
|
||||||
|
|
||||||
|
__all__ = ["Logic", "Plugins", "SearchResult"]
|
||||||
54
src/logic_plugin_manager/logic/logic.py
Normal file
54
src/logic_plugin_manager/logic/logic.py
Normal 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"]
|
||||||
248
src/logic_plugin_manager/logic/plugins.py
Normal file
248
src/logic_plugin_manager/logic/plugins.py
Normal 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"]
|
||||||
0
src/logic_plugin_manager/py.typed
Normal file
0
src/logic_plugin_manager/py.typed
Normal file
4
src/logic_plugin_manager/tags/__init__.py
Normal file
4
src/logic_plugin_manager/tags/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .musicapps import MusicApps, Properties, Tagpool
|
||||||
|
from .tagset import Tagset
|
||||||
|
|
||||||
|
__all__ = ["MusicApps", "Properties", "Tagpool", "Tagset"]
|
||||||
73
src/logic_plugin_manager/tags/musicapps.py
Normal file
73
src/logic_plugin_manager/tags/musicapps.py
Normal 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"]
|
||||||
43
src/logic_plugin_manager/tags/tagset.py
Normal file
43
src/logic_plugin_manager/tags/tagset.py
Normal 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
124
uv.lock
generated
Normal 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" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user