docs(global): add documentation

This commit is contained in:
h
2025-11-07 17:15:12 +01:00
parent ba3005d9b4
commit af227b597b
30 changed files with 2978 additions and 12 deletions

5
.gitignore vendored
View File

@@ -12,3 +12,8 @@ wheels/
# Development and testing # Development and testing
.idea .idea
t t
# Docs
docs/_build/
docs/_static/
docs/_templates/

22
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,22 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version, and other tools you might need
build:
os: ubuntu-24.04
tools:
python: "3.13"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py
# Optionally, but recommended,
# declare the Python requirements required to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt

View File

@@ -3,32 +3,83 @@
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE.md) [![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) [![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/) [![PyPI version](https://badge.fury.io/py/logic-plugin-manager.svg)](https://pypi.org/project/logic-plugin-manager/)
[![Python Version](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
A utility for parsing and managing plugins in Logic Pro. Programmatic management of Logic Pro audio plugins on macOS.
## 📦 Installation ## Overview
Logic Plugin Manager is a Python library that provides programmatic access to Logic Pro's plugin management system. It enables automated discovery, categorization, and organization of macOS Audio Unit plugins through Logic's internal tag database.
**Key Capabilities:**
- Automated plugin discovery and indexing
- Hierarchical category management
- Advanced search with fuzzy matching
- Bulk operations on plugin collections
- Programmatic metadata manipulation
## Requirements
- **Python**: 3.13 or higher
- **Operating System**: macOS
- **Dependencies**: None (core functionality), `rapidfuzz>=3.14.3` (optional, for fuzzy search)
## Installation
```bash ```bash
uv add logic-plugin-manager pip install logic-plugin-manager
``` ```
### With search functionality (includes rapidfuzz): For fuzzy search functionality:
```bash ```bash
uv add logic-plugin-manager[search] pip install logic-plugin-manager[search]
``` ```
## 🚀 Quick Start ## Usage
```python ```python
from logic_plugin_manager import Logic from logic_plugin_manager import Logic
# Initialize and discover plugins
logic = Logic() logic = Logic()
# Access plugin collection
for plugin in logic.plugins.all():
print(f"{plugin.full_name} - {plugin.type_name.display_name}")
# Search with scoring
results = logic.plugins.search("reverb", use_fuzzy=True, max_results=10)
# Category management
category = logic.introduce_category("Production Tools")
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
plugin.add_to_category(category)
``` ```
## 📄 License ## Architecture
The library is organized into three primary modules:
- **`components`**: Audio Unit component and bundle parsing
- **`logic`**: High-level plugin management interface
- **`tags`**: Category system and tag database operations
## Documentation
Full documentation available at: https://logic-plugin-manager.readthedocs.io
## License
This project is dual-licensed: This project is dual-licensed:
#### 🆓 Open Source (AGPL-3.0)
Free for open source projects. See [LICENSE.md](LICENSE.md). **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). **Commercial License**: Required for closed-source or commercial applications. See [LICENSE-COMMERCIAL.md](LICENSE-COMMERCIAL.md).
Contact: h@kotikot.com Contact: h@kotikot.com
## Links
- **Repository**: https://github.com/kotikotprojects/logic-plugin-manager
- **PyPI**: https://pypi.org/project/logic-plugin-manager/
- **Documentation**: https://logic-plugin-manager.readthedocs.io

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

58
docs/conf.py Normal file
View File

@@ -0,0 +1,58 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
import tomllib
sys.path.insert(0, os.path.abspath("."))
sys.path.insert(0, os.path.abspath("../."))
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "logic-plugin-manager"
copyright = "2025, h"
author = "h"
release = tomllib.load(open("../pyproject.toml", "rb"))["project"]["version"]
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinx_autodoc_typehints",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
autodoc_default_options = {
"members": True,
"member-order": "bysource",
"special-members": "__init__",
"undoc-members": True,
"exclude-members": "__weakref__",
"inherited-members": False,
}
autodoc_typehints = "description"
typehints_use_signature = True
typehints_use_signature_return = True
autodoc_member_order = "bysource"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "furo"
html_static_path = ["_static"]
html_title = "Logic Plugin Manager"
html_theme_options = {
"sidebar_hide_name": False,
"navigation_with_keys": True,
}

467
docs/core_concepts.rst Normal file
View File

@@ -0,0 +1,467 @@
Core Concepts
=============
This page explains the key concepts and architecture of Logic Plugin Manager.
Architecture Overview
---------------------
Logic Plugin Manager is structured around three main areas:
1. **Components**: Audio Unit plugin bundles and their metadata
2. **Logic Management**: High-level interface for plugin discovery and organization
3. **Tags & Categories**: Logic Pro's categorization system
The Library's Structure
~~~~~~~~~~~~~~~~~~~~~~~~
::
logic_plugin_manager/
├── components/ # Audio Unit components
│ ├── AudioComponent # Individual plugin representation
│ ├── Component # .component bundle parser
│ └── AudioUnitType # Audio Unit type enumeration
├── logic/ # Main management interface
│ ├── Logic # Primary entry point
│ ├── Plugins # Plugin collection with search
│ └── SearchResult # Search result with scoring
└── tags/ # Category and tag management
├── Category # Plugin category management
├── MusicApps # Database interface
├── Tagpool # Plugin count tracking
├── Properties # Category sorting
└── Tagset # Plugin tag files
Understanding Audio Components
------------------------------
Audio Component Bundle Structure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
macOS Audio Unit plugins are distributed as ``.component`` bundles:
::
PluginName.component/
└── Contents/
├── Info.plist # Metadata and component definitions
├── MacOS/
│ └── PluginName # Executable binary
└── Resources/ # UI resources, presets, etc.
The ``Info.plist`` file contains an ``AudioComponents`` array defining one or more Audio Units within the bundle.
AudioComponent Class
~~~~~~~~~~~~~~~~~~~~
Each Audio Unit is represented by an ``AudioComponent`` instance with:
- **Identification**: Type, subtype, and manufacturer codes
- **Metadata**: Name, description, version
- **Categories**: Tags assigned in Logic Pro
- **Tags ID**: Unique identifier derived from codes
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
plugin = logic.plugins.get_by_name("Pro-Q 3")
# Access component information
print(f"Full Name: {plugin.full_name}")
print(f"Manufacturer: {plugin.manufacturer}")
print(f"Type Code: {plugin.type_code}")
print(f"Subtype Code: {plugin.subtype_code}")
print(f"Manufacturer Code: {plugin.manufacturer_code}")
print(f"Tags ID: {plugin.tags_id}")
Audio Unit Types
~~~~~~~~~~~~~~~~
Logic Plugin Manager recognizes five Audio Unit types:
.. list-table::
:header-rows: 1
:widths: 15 25 20 40
* - Code
- Display Name
- Alternative Name
- Description
* - ``aufx``
- Audio FX
- Effect
- Audio effect processors
* - ``aumu``
- Instrument
- Music Device
- Software instruments and synthesizers
* - ``aumf``
- MIDI-controlled Effects
- Music Effect
- Effects controlled by MIDI input
* - ``aumi``
- MIDI FX
- MIDI Generator
- MIDI processors and generators
* - ``augn``
- Generator
- Generator
- Audio generators
.. code-block:: python
from logic_plugin_manager import AudioUnitType
# Get type by code
instrument_type = AudioUnitType.from_code("aumu")
print(instrument_type.display_name) # "Instrument"
# Search for types
effect_types = AudioUnitType.search("effect")
The Logic Class
---------------
The ``Logic`` class is the main entry point for plugin management.
Initialization Process
~~~~~~~~~~~~~~~~~~~~~~
When you create a ``Logic`` instance (with ``lazy=False``, the default):
1. Loads MusicApps database (tagpool and properties files)
2. Scans ``/Library/Audio/Plug-Ins/Components`` directory
3. Parses each ``.component`` bundle's ``Info.plist``
4. Creates ``AudioComponent`` instances
5. Loads tagsets and categories for each plugin
6. Indexes plugins for fast lookups
.. code-block:: python
from logic_plugin_manager import Logic
# Full initialization
logic = Logic() # Discovers everything
# Lazy initialization (manual control)
logic = Logic(lazy=True)
logic.discover_plugins() # When ready to load plugins
logic.discover_categories() # When ready to load categories
Logic Instance Attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
logic = Logic()
# Access discovered data
logic.plugins # Plugins collection
logic.components # Set of Component bundles
logic.categories # Dict of category_name -> Category
logic.musicapps # MusicApps database interface
# Configuration paths
logic.components_path # Path to Components directory
logic.tags_path # Path to tags database
Plugin Collection & Search
---------------------------
The Plugins Class
~~~~~~~~~~~~~~~~~
The ``Plugins`` class provides a searchable collection with multiple indexes:
- **By full name**: Exact manufacturer + plugin name match
- **By manufacturer**: All plugins from a vendor
- **By name**: Plugin name only
- **By codes**: Type, subtype, manufacturer codes
- **By category**: All plugins in a category
- **By tags_id**: Unique component identifier
.. code-block:: python
logic = Logic()
# Exact lookups (fast)
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
plugin = logic.plugins.get_by_tags_id("61756678-65517033-46466")
# Set lookups
fabfilter = logic.plugins.get_by_manufacturer("fabfilter")
effects = logic.plugins.get_by_type_code("aufx")
eq_plugins = logic.plugins.get_by_category("Effects:EQ")
Search Algorithm
~~~~~~~~~~~~~~~~
The ``search()`` method implements a sophisticated scoring system:
**Priority Levels** (highest to lowest):
1. **Tags ID exact match** (score: 1000)
2. **Name substring match** (850-900)
3. **Full name substring match** (750-800)
4. **Fuzzy name match** (650-700, requires rapidfuzz)
5. **Manufacturer match** (580-650)
6. **Category match** (480-550)
7. **Type code match** (380-450)
8. **Subtype code match** (290-350)
9. **Manufacturer code match** (190-250)
.. code-block:: python
logic = Logic()
# Fuzzy search with scoring
results = logic.plugins.search(
"serum",
use_fuzzy=True,
fuzzy_threshold=80,
max_results=10
)
for result in results:
print(f"{result.plugin.full_name}")
print(f" Score: {result.score}")
print(f" Matched: {result.match_field}")
Categories & Tags
-----------------
Hierarchical Structure
~~~~~~~~~~~~~~~~~~~~~~
Categories in Logic Pro use colon-separated hierarchies:
::
Effects
Effects:Dynamics
Effects:Dynamics:Compressor
Effects:EQ
Instruments
Instruments:Synth
.. code-block:: python
logic = Logic()
# Navigate hierarchy
dynamics = logic.categories["Effects:Dynamics"]
parent = dynamics.parent # "Effects" category
child = parent.child("EQ") # "Effects:EQ" category
# Check properties
print(dynamics.plugin_amount)
print(dynamics.is_root)
Category Operations
~~~~~~~~~~~~~~~~~~~
.. code-block:: python
logic = Logic()
# Create new category
my_cat = logic.introduce_category("Studio Essentials")
# Manage plugins
plugin = logic.plugins.get_by_name("Pro-Q 3")
plugin.add_to_category(my_cat)
plugin.remove_from_category(my_cat)
plugin.move_to_category(my_cat) # Removes from all others
# Bulk operations
plugins_set = {plugin1, plugin2, plugin3}
logic.add_plugins_to_category(my_cat, plugins_set)
# Update plugin count
logic.sync_category_plugin_amount(my_cat)
Category Sorting
~~~~~~~~~~~~~~~~
Categories have a sort order managed by the ``Properties`` database:
.. code-block:: python
category = logic.categories["Effects:EQ"]
# Check position
print(category.index)
print(category.is_first)
print(category.is_last)
prev, next = category.neighbors
# Reorder
category.move_up(steps=2)
category.move_down()
category.move_to_top()
category.move_to_bottom()
category.move_before(other_category)
category.move_after(other_category)
category.swap(other_category)
Tagsets
-------
What are Tagsets?
~~~~~~~~~~~~~~~~~
Each ``AudioComponent`` has an associated ``.tagset`` file stored at:
``~/Music/Audio Music Apps/Databases/Tags/<tags_id>.tagset``
These XML plist files store:
- **nickname**: Custom display name
- **shortname**: Abbreviated name for UI
- **tags**: Dictionary of category assignments
.. code-block:: python
logic = Logic()
plugin = logic.plugins.get_by_name("Pro-Q 3")
# Access tagset
print(plugin.tagset.nickname)
print(plugin.tagset.shortname)
print(plugin.tagset.tags) # {"Effects:EQ": "user"}
# Modify tagset
plugin.set_nickname("My Favorite EQ")
plugin.set_shortname("PQ3")
Tag Values
~~~~~~~~~~
Category tags typically have the value ``"user"`` indicating user assignment, but can have other values from Logic Pro's internal management.
MusicApps Database
------------------
Database Files
~~~~~~~~~~~~~~
Logic Pro stores category information in two files:
**MusicApps.tagpool**
Maps category names to plugin counts:
.. code-block:: python
{
"Effects": 245,
"Effects:EQ": 18,
"Instruments": 89,
...
}
**MusicApps.properties**
Stores category sort order and preferences:
.. code-block:: python
{
"sorting": ["Effects", "Effects:Dynamics", ...],
"user_sorted": "property" # or absent for alphabetical
}
Accessing the Database
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
logic = Logic()
musicapps = logic.musicapps
# Access tagpool
print(musicapps.tagpool.categories)
# Access properties
print(musicapps.properties.sorting)
print(musicapps.properties.user_sorted)
# Modify database
musicapps.introduce_category("New Category")
musicapps.remove_category("Old Category")
Thread Safety
-------------
Logic Plugin Manager is **not thread-safe**. The library performs file I/O operations on Logic Pro's database files without locking mechanisms.
**Recommendations:**
- Use a single ``Logic`` instance per process
- Avoid concurrent writes to categories or tagsets
- Reload data after external changes (e.g., Logic Pro modifying categories)
Performance Considerations
--------------------------
Initial Discovery
~~~~~~~~~~~~~~~~~
Loading all plugins can take several seconds depending on the number of installed components. Use ``lazy=True`` for faster startup when you don't need immediate access.
Indexing
~~~~~~~~
The ``Plugins`` collection maintains multiple indexes. Reindexing is required if you:
- Modify plugin properties externally
- Add plugins after initialization
.. code-block:: python
logic = Logic(lazy=True)
logic.discover_plugins()
# ... modify plugins externally ...
logic.plugins.reindex_all()
Search Performance
~~~~~~~~~~~~~~~~~~
- Exact lookups by indexes are O(1)
- Fuzzy search with ``rapidfuzz`` is O(n) but optimized
- Use ``max_results`` to limit computation for large result sets
Best Practices
--------------
1. **Single Instance**: Create one ``Logic`` instance and reuse it
2. **Error Handling**: Wrap operations in try-except blocks for specific exceptions
3. **Lazy Loading**: Use for CLI tools or services that don't need immediate access
4. **Reloading**: Call ``load()`` methods after external modifications to databases
.. code-block:: python
from logic_plugin_manager import (
Logic,
CategoryValidationError,
PluginLoadError
)
try:
logic = Logic()
except PluginLoadError as e:
print(f"Failed to load plugins: {e}")
# Handle gracefully
# Batch operation example
favorites = {
logic.plugins.get_by_name(name)
for name in ["Pro-Q 3", "Serum", "Valhalla VintageVerb"]
if logic.plugins.get_by_name(name)
}
if favorites:
fav_category = logic.introduce_category("Favorites")
logic.add_plugins_to_category(fav_category, favorites)
logic.sync_category_plugin_amount(fav_category)

712
docs/examples.rst Normal file
View File

@@ -0,0 +1,712 @@
Usage Examples
==============
This page provides practical examples for common use cases.
Plugin Discovery & Inspection
------------------------------
List All Plugins
~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
print(f"Total plugins: {len(logic.plugins.all())}")
for plugin in logic.plugins.all():
print(f"{plugin.full_name}")
print(f" Type: {plugin.type_name.display_name}")
print(f" Version: {plugin.version}")
print(f" Categories: {', '.join(c.name for c in plugin.categories)}")
print()
Filter by Type
~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get all instruments
instruments = logic.plugins.get_by_type_code("aumu")
print(f"Found {len(instruments)} instruments")
# Get all effects
effects = logic.plugins.get_by_type_code("aufx")
print(f"Found {len(effects)} effects")
Find Plugins by Manufacturer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# List all manufacturers
manufacturers = set()
for plugin in logic.plugins.all():
manufacturers.add(plugin.manufacturer)
for mfr in sorted(manufacturers):
plugins = logic.plugins.get_by_manufacturer(mfr)
print(f"{mfr}: {len(plugins)} plugins")
# Get specific manufacturer's plugins
fabfilter = logic.plugins.get_by_manufacturer("fabfilter")
for plugin in fabfilter:
print(f" - {plugin.name}")
Inspect Plugin Details
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
if plugin:
print(f"Full Name: {plugin.full_name}")
print(f"Manufacturer: {plugin.manufacturer}")
print(f"Name: {plugin.name}")
print(f"Description: {plugin.description}")
print(f"Type: {plugin.type_name.display_name} ({plugin.type_code})")
print(f"Subtype: {plugin.subtype_code}")
print(f"Manufacturer Code: {plugin.manufacturer_code}")
print(f"Version: {plugin.version}")
print(f"Factory Function: {plugin.factory_function}")
print(f"Tags ID: {plugin.tags_id}")
print(f"Tagset Path: {plugin.tagset.path}")
if plugin.tagset.nickname:
print(f"Nickname: {plugin.tagset.nickname}")
if plugin.tagset.shortname:
print(f"Short Name: {plugin.tagset.shortname}")
print(f"Categories: {', '.join(c.name for c in plugin.categories)}")
Search & Discovery
------------------
Simple Text Search
~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Simple substring search
reverb_plugins = logic.plugins.search_simple("reverb")
print(f"Found {len(reverb_plugins)} plugins with 'reverb' in name")
for plugin in reverb_plugins:
print(f" - {plugin.full_name}")
Advanced Fuzzy Search
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Fuzzy search with scoring
results = logic.plugins.search(
query="compressor",
use_fuzzy=True,
fuzzy_threshold=80,
max_results=10
)
for i, result in enumerate(results, 1):
print(f"{i}. {result.plugin.full_name}")
print(f" Score: {result.score:.1f}")
print(f" Matched field: {result.match_field}")
print()
Search by Multiple Criteria
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
def find_plugins(search_term, plugin_type=None, category=None):
"""Search plugins with optional filters."""
results = logic.plugins.search(search_term, use_fuzzy=True)
plugins = [r.plugin for r in results]
# Filter by type if specified
if plugin_type:
plugins = [p for p in plugins if p.type_code == plugin_type]
# Filter by category if specified
if category:
plugins = [
p for p in plugins
if any(c.name == category for c in p.categories)
]
return plugins
# Find reverb effects (not instruments)
reverbs = find_plugins("reverb", plugin_type="aufx")
# Find synthesizers in Instruments category
synths = find_plugins("synth", plugin_type="aumu", category="Instruments")
Category Management
-------------------
List All Categories
~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
print("Categories:")
for name, category in sorted(logic.categories.items()):
print(f" {name} ({category.plugin_amount} plugins)")
Create Category Hierarchy
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Create nested categories
categories_to_create = [
"My Plugins",
"My Plugins:Favorites",
"My Plugins:Favorites:Mixing",
"My Plugins:Favorites:Mastering",
]
for cat_name in categories_to_create:
if cat_name not in logic.categories:
category = logic.introduce_category(cat_name)
print(f"Created: {cat_name}")
else:
print(f"Already exists: {cat_name}")
Add Plugins to Category
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Create or get category
favorites = logic.categories.get("Favorites")
if not favorites:
favorites = logic.introduce_category("Favorites")
# Add single plugin
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
if plugin:
plugin.add_to_category(favorites)
print(f"Added {plugin.full_name} to {favorites.name}")
# Update category count
logic.sync_category_plugin_amount(favorites)
Bulk Category Assignment
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Find all FabFilter plugins
fabfilter_plugins = logic.plugins.get_by_manufacturer("fabfilter")
# Create category
fabfilter_cat = logic.categories.get("FabFilter")
if not fabfilter_cat:
fabfilter_cat = logic.introduce_category("FabFilter")
# Bulk add
logic.add_plugins_to_category(fabfilter_cat, fabfilter_plugins)
print(f"Added {len(fabfilter_plugins)} FabFilter plugins")
Move Plugins Between Categories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get plugins from one category
dynamics_plugins = logic.plugins.get_by_category("Effects:Dynamics")
# Filter for compressors
compressors = {
p for p in dynamics_plugins
if "compress" in p.name.lower()
}
# Move to specific category
comp_category = logic.categories.get("Effects:Dynamics:Compressor")
if comp_category:
logic.move_plugins_to_category(comp_category, compressors)
logic.sync_category_plugin_amount(comp_category)
Organize Uncategorized Plugins
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get plugins without categories
uncategorized = logic.plugins.get_by_category(None)
print(f"Found {len(uncategorized)} uncategorized plugins")
# Auto-categorize by manufacturer
for plugin in uncategorized:
manufacturer = plugin.manufacturer.strip()
category_name = f"By Manufacturer:{manufacturer}"
# Create category if needed
if category_name not in logic.categories:
logic.introduce_category(category_name)
category = logic.categories[category_name]
plugin.add_to_category(category)
# Update all category counts
logic.sync_all_categories_plugin_amount()
Category Sorting
~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get category
category = logic.categories["Effects:EQ"]
# Check current position
print(f"Current index: {category.index}")
prev, next_cat = category.neighbors
if prev:
print(f"Previous: {prev.name}")
if next_cat:
print(f"Next: {next_cat.name}")
# Move category
category.move_to_top()
category.move_up(steps=5)
category.move_down()
# Move relative to another category
target = logic.categories["Effects:Delay"]
category.move_before(target)
Custom Plugin Metadata
----------------------
Set Nicknames
~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Set custom nicknames for easier identification
plugins_to_rename = {
"fabfilter: pro-q 3": "PQ3 - Main EQ",
"fabfilter: pro-c 2": "PC2 - Main Compressor",
"valhalla shimmer": "Shimmer - Ambient Verb",
}
for full_name, nickname in plugins_to_rename.items():
plugin = logic.plugins.get_by_full_name(full_name)
if plugin:
plugin.set_nickname(nickname)
print(f"Renamed: {full_name} -> {nickname}")
Set Short Names
~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Set short names for UI display
shortnames = {
"fabfilter: pro-q 3": "PQ3",
"fabfilter: pro-c 2": "PC2",
"serum": "SRM",
}
for full_name, shortname in shortnames.items():
plugin = logic.plugins.get_by_full_name(full_name)
if plugin:
plugin.set_shortname(shortname)
Batch Metadata Updates
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Add manufacturer prefix to all plugin nicknames
for plugin in logic.plugins.all():
if not plugin.tagset.nickname:
manufacturer = plugin.manufacturer.upper()
nickname = f"[{manufacturer}] {plugin.name}"
plugin.set_nickname(nickname)
print(f"Set nickname for {plugin.full_name}")
Advanced Operations
-------------------
Export Plugin Inventory
~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
import csv
from logic_plugin_manager import Logic
logic = Logic()
# Export to CSV
with open("plugin_inventory.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"Full Name", "Manufacturer", "Type", "Version",
"Categories", "Subtype Code", "Tags ID"
])
for plugin in sorted(logic.plugins.all(), key=lambda p: p.full_name):
writer.writerow([
plugin.full_name,
plugin.manufacturer,
plugin.type_name.display_name,
plugin.version,
"; ".join(c.name for c in plugin.categories),
plugin.subtype_code,
plugin.tags_id,
])
print("Exported plugin inventory to plugin_inventory.csv")
Clone Category Structure
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
def clone_category(source: str, target: str, logic: Logic):
"""Clone plugins from source category to target category."""
# Get plugins in source
source_plugins = logic.plugins.get_by_category(source)
# Create target if needed
if target not in logic.categories:
logic.introduce_category(target)
target_category = logic.categories[target]
# Add plugins to target
logic.add_plugins_to_category(target_category, source_plugins)
print(f"Cloned {len(source_plugins)} plugins from {source} to {target}")
logic = Logic()
clone_category("Effects:EQ", "My Plugins:EQ", logic)
Find Duplicate Plugins
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from collections import defaultdict
from logic_plugin_manager import Logic
logic = Logic()
# Group by name (ignoring manufacturer)
by_name = defaultdict(list)
for plugin in logic.plugins.all():
by_name[plugin.name.lower()].append(plugin)
# Find duplicates
duplicates = {
name: plugins
for name, plugins in by_name.items()
if len(plugins) > 1
}
print(f"Found {len(duplicates)} plugin names with multiple versions:\n")
for name, plugins in sorted(duplicates.items()):
print(f"{name}:")
for plugin in plugins:
print(f" - {plugin.full_name} (v{plugin.version})")
print()
Backup and Restore Categories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
import json
from pathlib import Path
from logic_plugin_manager import Logic
def backup_categories(output_file: Path):
"""Backup all plugin category assignments."""
logic = Logic()
backup_data = {}
for plugin in logic.plugins.all():
backup_data[plugin.tags_id] = {
"full_name": plugin.full_name,
"categories": [c.name for c in plugin.categories],
}
with open(output_file, "w") as f:
json.dump(backup_data, f, indent=2)
print(f"Backed up {len(backup_data)} plugin assignments")
def restore_categories(backup_file: Path):
"""Restore plugin category assignments from backup."""
logic = Logic()
with open(backup_file) as f:
backup_data = json.load(f)
restored = 0
for tags_id, data in backup_data.items():
plugin = logic.plugins.get_by_tags_id(tags_id)
if plugin:
# Restore categories
categories = [
logic.categories[name]
for name in data["categories"]
if name in logic.categories
]
if categories:
plugin.set_categories(categories)
restored += 1
print(f"Restored {restored} plugin assignments")
# Usage
backup_categories(Path("categories_backup.json"))
# restore_categories(Path("categories_backup.json"))
Generate Category Report
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
print("=" * 60)
print("CATEGORY REPORT")
print("=" * 60)
print()
# Sort categories by hierarchy
sorted_categories = sorted(logic.categories.items())
for name, category in sorted_categories:
# Calculate depth based on colons
depth = name.count(":")
indent = " " * depth
# Get plugins in this exact category
plugins = logic.plugins.get_by_category(name)
print(f"{indent}├─ {name.split(':')[-1]}")
print(f"{indent}│ Count: {category.plugin_amount}")
print(f"{indent}│ Index: {category.index}")
if plugins:
print(f"{indent}│ Plugins:")
for plugin in sorted(plugins, key=lambda p: p.full_name)[:5]:
print(f"{indent}│ - {plugin.full_name}")
if len(plugins) > 5:
print(f"{indent}│ ... and {len(plugins) - 5} more")
print()
Working with Component Bundles
-------------------------------
Inspect Component Bundles
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
print(f"Total component bundles: {len(logic.components)}\n")
for component in logic.components:
print(f"Bundle: {component.name}")
print(f" ID: {component.bundle_id}")
print(f" Version: {component.version} ({component.short_version})")
print(f" Audio Components: {len(component.audio_components)}")
for audio_comp in component.audio_components:
print(f" - {audio_comp.full_name}")
print()
Find Multi-Plugin Bundles
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Find bundles with multiple plugins
multi_plugin_bundles = [
comp for comp in logic.components
if len(comp.audio_components) > 1
]
print(f"Found {len(multi_plugin_bundles)} bundles with multiple plugins:\n")
for component in multi_plugin_bundles:
print(f"{component.name} - {len(component.audio_components)} plugins:")
for plugin in component.audio_components:
print(f" - {plugin.name} ({plugin.type_name.display_name})")
print()
Error Handling Examples
-----------------------
Robust Plugin Search
~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic, PluginLoadError
def safe_find_plugin(plugin_name: str) -> None:
"""Safely search for a plugin with error handling."""
try:
logic = Logic()
except PluginLoadError as e:
print(f"Error loading plugins: {e}")
return
plugin = logic.plugins.get_by_full_name(plugin_name)
if plugin:
print(f"Found: {plugin.full_name}")
print(f"Type: {plugin.type_name.display_name}")
# Try to load tagset
try:
print(f"Categories: {', '.join(c.name for c in plugin.categories)}")
except Exception as e:
print(f"Could not load categories: {e}")
else:
print(f"Plugin '{plugin_name}' not found")
# Try fuzzy search
results = logic.plugins.search(plugin_name)
if results:
print(f"\nDid you mean:")
for result in results[:3]:
print(f" - {result.plugin.full_name}")
safe_find_plugin("fabfilter: pro-q 3")
Graceful Category Operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import (
Logic,
CategoryValidationError,
MusicAppsLoadError
)
def safe_add_to_category(plugin_name: str, category_name: str):
"""Add plugin to category with error handling."""
try:
logic = Logic()
except MusicAppsLoadError as e:
print(f"Database error: {e}")
return
# Find plugin
plugin = logic.plugins.get_by_full_name(plugin_name)
if not plugin:
print(f"Plugin '{plugin_name}' not found")
return
# Get or create category
try:
category = logic.categories[category_name]
except KeyError:
try:
category = logic.introduce_category(category_name)
print(f"Created new category: {category_name}")
except Exception as e:
print(f"Could not create category: {e}")
return
# Add to category
try:
plugin.add_to_category(category)
logic.sync_category_plugin_amount(category)
print(f"Added {plugin.full_name} to {category_name}")
except Exception as e:
print(f"Error adding to category: {e}")
safe_add_to_category("fabfilter: pro-q 3", "My Favorites")

240
docs/getting_started.rst Normal file
View File

@@ -0,0 +1,240 @@
Getting Started
===============
This guide will help you get started with Logic Plugin Manager, a Python library for programmatically managing Logic Pro's audio plugins.
Installation
------------
Basic Installation
~~~~~~~~~~~~~~~~~~
Install the package using pip or uv:
.. code-block:: bash
pip install logic-plugin-manager
Or with uv:
.. code-block:: bash
uv add logic-plugin-manager
With Search Functionality
~~~~~~~~~~~~~~~~~~~~~~~~~~
For fuzzy search capabilities, install with the ``search`` extra:
.. code-block:: bash
pip install logic-plugin-manager[search]
# or
uv add logic-plugin-manager[search]
This includes the ``rapidfuzz`` dependency for advanced plugin searching.
Requirements
------------
- **Python**: 3.13 or higher
- **Operating System**: macOS only (Logic Pro specific)
- **Logic Pro**: Installed with audio plugins
The library accesses:
- Audio Components directory: ``/Library/Audio/Plug-Ins/Components``
- Tags database: ``~/Music/Audio Music Apps/Databases/Tags``
Quick Start
-----------
Basic Usage
~~~~~~~~~~~
The simplest way to start is by creating a ``Logic`` instance:
.. code-block:: python
from logic_plugin_manager import Logic
# Initialize and discover all plugins
logic = Logic()
# Access all plugins
for plugin in logic.plugins.all():
print(f"{plugin.full_name} - {plugin.type_name.display_name}")
# Access categories
for category_name, category in logic.categories.items():
print(f"{category_name}: {category.plugin_amount} plugins")
Lazy Loading
~~~~~~~~~~~~
For faster initialization when you don't need immediate access to all plugins:
.. code-block:: python
from logic_plugin_manager import Logic
# Initialize without loading plugins
logic = Logic(lazy=True)
# Manually discover plugins when needed
logic.discover_plugins()
logic.discover_categories()
Searching Plugins
~~~~~~~~~~~~~~~~~
Search for plugins by name, manufacturer, or category:
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Simple substring search
results = logic.plugins.search_simple("reverb")
# Advanced fuzzy search with scoring
results = logic.plugins.search("serum", use_fuzzy=True)
for result in results[:5]: # Top 5 results
print(f"{result.plugin.full_name} (score: {result.score})")
Working with Categories
~~~~~~~~~~~~~~~~~~~~~~~
Organize plugins into categories:
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get plugins in a specific category
effects = logic.plugins.get_by_category("Effects")
# Get or create a category
my_category = logic.categories.get("My Favorites")
if not my_category:
my_category = logic.introduce_category("My Favorites")
# Add plugin to category
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
if plugin:
plugin.add_to_category(my_category)
Custom Paths
~~~~~~~~~~~~
If your Logic Pro or components are in non-standard locations:
.. code-block:: python
from pathlib import Path
from logic_plugin_manager import Logic
logic = Logic(
components_path=Path("/custom/path/to/Components"),
tags_path=Path("~/custom/path/to/Tags").expanduser()
)
Next Steps
----------
- Learn about :doc:`core_concepts` to understand the library's architecture
- Explore :doc:`examples` for common use cases
- Check the :doc:`logic_plugin_manager` for detailed API reference
Common Patterns
---------------
Finding a Specific Plugin
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# By full name (exact match)
plugin = logic.plugins.get_by_full_name("apple: logic eq")
# By manufacturer
fabfilter_plugins = logic.plugins.get_by_manufacturer("fabfilter")
# By audio unit type
instruments = logic.plugins.get_by_type_code("aumu")
Batch Category Operations
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get all synthesizer plugins
synths = logic.plugins.search("synth", use_fuzzy=True)
synth_plugins = {result.plugin for result in synths[:20]}
# Move them to a custom category
synth_category = logic.introduce_category("Synthesizers")
logic.move_plugins_to_category(synth_category, synth_plugins)
Working with Plugin Metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
plugin = logic.plugins.get_by_name("Pro-Q 3")
if plugin:
# Access metadata
print(f"Manufacturer: {plugin.manufacturer}")
print(f"Type: {plugin.type_name.display_name}")
print(f"Version: {plugin.version}")
print(f"Categories: {[c.name for c in plugin.categories]}")
# Set custom nickname
plugin.set_nickname("My Favorite EQ")
# Set short name for UI display
plugin.set_shortname("PQ3")
Error Handling
--------------
The library raises specific exceptions for different error conditions:
.. code-block:: python
from logic_plugin_manager import (
Logic,
PluginLoadError,
MusicAppsLoadError,
CategoryValidationError
)
try:
logic = Logic()
except MusicAppsLoadError as e:
print(f"Could not load Logic's database: {e}")
except PluginLoadError as e:
print(f"Error loading plugins: {e}")
try:
category = logic.categories["Nonexistent"]
except KeyError:
print("Category not found")
See :doc:`logic_plugin_manager` for all exception types.

91
docs/index.rst Normal file
View File

@@ -0,0 +1,91 @@
Logic Plugin Manager
====================
**Programmatic management of Logic Pro audio plugins**
Logic Plugin Manager is a Python library for discovering, organizing, and managing macOS Audio Unit plugins used by Logic Pro. It provides programmatic access to Logic's internal tag database, enabling automated plugin organization, bulk categorization, and advanced search capabilities.
----
Features
--------
- **Plugin Discovery**: Automatically scan and index all installed Audio Unit plugins
- **Category Management**: Create, modify, and organize plugin categories programmatically
- **Advanced Search**: Fuzzy search with scoring across multiple attributes
- **Bulk Operations**: Efficiently manage large plugin collections
- **Metadata Control**: Set custom nicknames, short names, and categories
- **Type-Safe**: Fully typed Python API with comprehensive documentation
Quick Example
-------------
.. code-block:: python
from logic_plugin_manager import Logic
# Initialize and discover all plugins
logic = Logic()
# Search for plugins
results = logic.plugins.search("reverb", use_fuzzy=True)
for result in results[:5]:
print(f"{result.plugin.full_name} (score: {result.score})")
# Organize into categories
favorites = logic.introduce_category("Favorites")
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
plugin.add_to_category(favorites)
Installation
------------
.. code-block:: bash
pip install logic-plugin-manager
# With search functionality
pip install logic-plugin-manager[search]
**Requirements**: Python 3.13+, macOS, Logic Pro
----
Documentation
=============
.. toctree::
:maxdepth: 2
:caption: User Guide
getting_started
core_concepts
examples
.. toctree::
:maxdepth: 2
:caption: API Reference
logic_plugin_manager
----
Indices
=======
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
----
License
=======
This project is dual-licensed:
- **Open Source (AGPL-3.0)**: Free for open source projects
- **Commercial License**: Available for closed-source/commercial use
Contact: h@kotikot.com

View File

@@ -0,0 +1,20 @@
Components
==========
The components package provides classes for working with macOS Audio Unit components and bundles.
AudioComponent
--------------
.. automodule:: logic_plugin_manager.components.audiocomponent
:members:
:show-inheritance:
:undoc-members:
Component
---------
.. automodule:: logic_plugin_manager.components.component
:members:
:show-inheritance:
:undoc-members:

View File

@@ -0,0 +1,20 @@
Logic Management
================
The logic package provides the main interface for plugin management and search functionality.
Logic Class
-----------
.. automodule:: logic_plugin_manager.logic.logic
:members:
:show-inheritance:
:undoc-members:
Plugins Collection
------------------
.. automodule:: logic_plugin_manager.logic.plugins
:members:
:show-inheritance:
:undoc-members:

View File

@@ -0,0 +1,28 @@
API Reference
=============
Core Modules
------------
.. toctree::
:maxdepth: 2
logic_plugin_manager.components
logic_plugin_manager.logic
logic_plugin_manager.tags
Defaults
--------
.. automodule:: logic_plugin_manager.defaults
:members:
:show-inheritance:
:undoc-members:
Exceptions
----------
.. automodule:: logic_plugin_manager.exceptions
:members:
:show-inheritance:
:undoc-members:

View File

@@ -0,0 +1,28 @@
Tags & Categories
=================
The tags package manages Logic Pro's category system and tag database.
Category Management
-------------------
.. automodule:: logic_plugin_manager.tags.category
:members:
:show-inheritance:
:undoc-members:
MusicApps Database
------------------
.. automodule:: logic_plugin_manager.tags.musicapps
:members:
:show-inheritance:
:undoc-members:
Tagset Files
------------
.. automodule:: logic_plugin_manager.tags.tagset
:members:
:show-inheritance:
:undoc-members:

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

3
docs/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
sphinx
furo
sphinx-autodoc-typehints

View File

@@ -47,6 +47,9 @@ build-backend = "uv_build"
dev = [ dev = [
"rich>=14.2.0", "rich>=14.2.0",
"rapidfuzz>=3.14.3", "rapidfuzz>=3.14.3",
"sphinx>=8.2.3",
"sphinx-autodoc-typehints>=3.5.2",
"furo>=2025.9.25",
] ]
[tool.isort] [tool.isort]

View File

@@ -1,3 +1,23 @@
"""Logic Plugin Manager - Programmatic management of Logic Pro audio plugins.
This library provides tools for discovering, categorizing, and managing
macOS Audio Unit plugins used by Logic Pro. It interfaces with Logic's
internal tag database to enable automated plugin organization.
Main Classes:
Logic: Primary interface for plugin discovery and management.
AudioComponent: Represents a single Audio Unit plugin.
Component: Represents a .component bundle.
Category: Represents a Logic Pro plugin category.
Plugins: Collection with indexed search capabilities.
Example:
>>> from logic_plugin_manager import Logic
>>> logic = Logic()
>>> for plugin in logic.plugins.all():
... print(plugin.full_name)
"""
import logging import logging
from .components import AudioComponent, AudioUnitType, Component from .components import AudioComponent, AudioUnitType, Component

View File

@@ -1,3 +1,9 @@
"""Components package for Audio Unit management.
This package provides classes for working with macOS Audio Component bundles
and their associated Audio Units.
"""
from .audiocomponent import AudioComponent, AudioUnitType from .audiocomponent import AudioComponent, AudioUnitType
from .component import Component from .component import Component

View File

@@ -1,3 +1,9 @@
"""Audio Component representation and management.
This module provides classes for working with macOS Audio Unit components,
including parsing component metadata and managing their tags and categories.
"""
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
@@ -11,6 +17,11 @@ logger = logging.getLogger(__name__)
class AudioUnitType(Enum): class AudioUnitType(Enum):
"""Enumeration of Audio Unit types supported by macOS.
Each enum value contains a tuple of (code, display_name, alt_name).
"""
AUFX = ("aufx", "Audio FX", "Effect") AUFX = ("aufx", "Audio FX", "Effect")
AUMU = ("aumu", "Instrument", "Music Device") AUMU = ("aumu", "Instrument", "Music Device")
AUMF = ("aumf", "MIDI-controlled Effects", "Music Effect") AUMF = ("aumf", "MIDI-controlled Effects", "Music Effect")
@@ -19,18 +30,41 @@ class AudioUnitType(Enum):
@property @property
def code(self) -> str: def code(self) -> str:
"""Get the four-character code for this Audio Unit type.
Returns:
str: Four-character type code (e.g., 'aufx', 'aumu').
"""
return self.value[0] return self.value[0]
@property @property
def display_name(self) -> str: def display_name(self) -> str:
"""Get the human-readable display name for this Audio Unit type.
Returns:
str: Display name (e.g., 'Audio FX', 'Instrument').
"""
return self.value[1] return self.value[1]
@property @property
def alt_name(self) -> str: def alt_name(self) -> str:
"""Get the alternative name for this Audio Unit type.
Returns:
str: Alternative name (e.g., 'Effect', 'Music Device').
"""
return self.value[2] return self.value[2]
@classmethod @classmethod
def from_code(cls, code: str) -> "AudioUnitType | None": def from_code(cls, code: str) -> "AudioUnitType | None":
"""Find an AudioUnitType by its four-character code.
Args:
code: Four-character type code (case-insensitive).
Returns:
AudioUnitType | None: Matching AudioUnitType or None if not found.
"""
code_lower = code.lower() code_lower = code.lower()
for unit_type in cls: for unit_type in cls:
if unit_type.code == code_lower: if unit_type.code == code_lower:
@@ -39,6 +73,16 @@ class AudioUnitType(Enum):
@classmethod @classmethod
def search(cls, query: str) -> list["AudioUnitType"]: def search(cls, query: str) -> list["AudioUnitType"]:
"""Search for AudioUnitTypes matching a query string.
Searches across code, display_name, and alt_name fields.
Args:
query: Search query string (case-insensitive).
Returns:
list[AudioUnitType]: List of matching AudioUnitType values.
"""
query_lower = query.lower() query_lower = query.lower()
results = [] results = []
for unit_type in cls: for unit_type in cls:
@@ -53,6 +97,27 @@ class AudioUnitType(Enum):
@dataclass @dataclass
class AudioComponent: class AudioComponent:
"""Represents a single Audio Unit component.
An AudioComponent encapsulates metadata about an Audio Unit plugin,
including its type, manufacturer, version, and associated tags/categories.
Attributes:
full_name: Full name in format 'Manufacturer: Plugin Name'.
manufacturer: Manufacturer/vendor name.
name: Plugin name (without manufacturer prefix).
manufacturer_code: Four-character manufacturer code.
description: Plugin description text.
factory_function: Name of the factory function.
type_code: Four-character Audio Unit type code.
type_name: AudioUnitType enum value.
subtype_code: Four-character subtype code.
version: Plugin version number.
tags_id: Unique identifier for tagset lookup.
tagset: Associated Tagset containing tags and metadata.
categories: List of Category objects this plugin belongs to.
"""
full_name: str full_name: str
manufacturer: str manufacturer: str
name: str name: str
@@ -75,6 +140,19 @@ class AudioComponent:
tags_path: Path = defaults.tags_path, tags_path: Path = defaults.tags_path,
musicapps: MusicApps = None, musicapps: MusicApps = None,
): ):
"""Initialize an AudioComponent from component data dictionary.
Args:
data: Dictionary containing component metadata from Info.plist.
lazy: If True, defer loading tagset and categories until needed.
tags_path: Path to tags database directory.
musicapps: Shared MusicApps instance for category management.
Raises:
CannotParseComponentError: If required fields are missing or malformed.
This can wrap KeyError, IndexError, AttributeError, UnicodeEncodeError,
or ValueError from data extraction operations.
"""
self.tags_path = tags_path self.tags_path = tags_path
self.lazy = lazy self.lazy = lazy
self.musicapps = musicapps or MusicApps(tags_path=self.tags_path, lazy=lazy) self.musicapps = musicapps or MusicApps(tags_path=self.tags_path, lazy=lazy)
@@ -105,6 +183,19 @@ class AudioComponent:
self.load() self.load()
def load(self) -> "AudioComponent": def load(self) -> "AudioComponent":
"""Load tagset and categories for this component.
Loads the component's tagset from disk and initializes Category objects
for all tags. Invalid categories are logged as warnings and skipped.
Returns:
AudioComponent: Self for method chaining.
Raises:
NonexistentTagsetError: If tagset file doesn't exist (from Tagset).
CannotParseTagsetError: If tagset file cannot be parsed (from Tagset).
MusicAppsLoadError: If MusicApps database files cannot be loaded (from Category).
"""
logger.debug(f"Loading AudioComponent {self.full_name}") logger.debug(f"Loading AudioComponent {self.full_name}")
self.tagset = Tagset(self.tags_path / self.tags_id, lazy=self.lazy) self.tagset = Tagset(self.tags_path / self.tags_id, lazy=self.lazy)
logger.debug(f"Loaded Tagset for {self.full_name}") logger.debug(f"Loaded Tagset for {self.full_name}")
@@ -123,44 +214,150 @@ class AudioComponent:
return self return self
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
"""Check equality based on tags_id.
Args:
other: Object to compare with.
Returns:
bool: True if both have the same tags_id, NotImplemented otherwise.
"""
if not isinstance(other, AudioComponent): if not isinstance(other, AudioComponent):
return NotImplemented return NotImplemented
return self.tags_id == other.tags_id return self.tags_id == other.tags_id
def __hash__(self): def __hash__(self):
"""Return hash based on tags_id for use in sets and dicts.
Returns:
int: Hash value.
"""
return hash(self.tags_id) return hash(self.tags_id)
def set_nickname(self, nickname: str) -> "AudioComponent": def set_nickname(self, nickname: str) -> "AudioComponent":
"""Set a custom nickname for this component.
Args:
nickname: Custom nickname string.
Returns:
AudioComponent: Self for method chaining.
Raises:
NonexistentTagsetError: If tagset file doesn't exist (from Tagset.set_nickname).
CannotParseTagsetError: If tagset file cannot be parsed (from Tagset.set_nickname).
TagsetWriteError: If writing tagset fails (from Tagset.set_nickname).
"""
self.tagset.set_nickname(nickname) self.tagset.set_nickname(nickname)
self.load() self.load()
return self return self
def set_shortname(self, shortname: str) -> "AudioComponent": def set_shortname(self, shortname: str) -> "AudioComponent":
"""Set a custom short name for this component.
Args:
shortname: Custom short name string.
Returns:
AudioComponent: Self for method chaining.
Raises:
NonexistentTagsetError: If tagset file doesn't exist (from Tagset.set_shortname).
CannotParseTagsetError: If tagset file cannot be parsed (from Tagset.set_shortname).
TagsetWriteError: If writing tagset fails (from Tagset.set_shortname).
"""
self.tagset.set_shortname(shortname) self.tagset.set_shortname(shortname)
self.load() self.load()
return self return self
def set_categories(self, categories: list[Category]) -> "AudioComponent": def set_categories(self, categories: list[Category]) -> "AudioComponent":
"""Replace all categories with the provided list.
Args:
categories: List of Category objects to assign.
Returns:
AudioComponent: Self for method chaining.
Raises:
NonexistentTagsetError: If tagset file doesn't exist (from Tagset.set_tags).
CannotParseTagsetError: If tagset file cannot be parsed (from Tagset.set_tags).
TagsetWriteError: If writing tagset fails (from Tagset.set_tags).
"""
self.tagset.set_tags({category.name: "user" for category in categories}) self.tagset.set_tags({category.name: "user" for category in categories})
self.load() self.load()
return self return self
def add_to_category(self, category: Category) -> "AudioComponent": def add_to_category(self, category: Category) -> "AudioComponent":
"""Add this component to a category.
Args:
category: Category to add this component to.
Returns:
AudioComponent: Self for method chaining.
Raises:
NonexistentTagsetError: If tagset file doesn't exist (from Tagset.add_tag).
CannotParseTagsetError: If tagset file cannot be parsed (from Tagset.add_tag).
TagsetWriteError: If writing tagset fails (from Tagset.add_tag).
"""
self.tagset.add_tag(category.name, "user") self.tagset.add_tag(category.name, "user")
self.load() self.load()
return self return self
def remove_from_category(self, category: Category) -> "AudioComponent": def remove_from_category(self, category: Category) -> "AudioComponent":
"""Remove this component from a category.
Args:
category: Category to remove this component from.
Returns:
AudioComponent: Self for method chaining.
Raises:
NonexistentTagsetError: If tagset file doesn't exist (from Tagset.remove_tag).
CannotParseTagsetError: If tagset file cannot be parsed (from Tagset.remove_tag).
KeyError: If tag doesn't exist (from Tagset.remove_tag).
TagsetWriteError: If writing tagset fails (from Tagset.remove_tag).
"""
self.tagset.remove_tag(category.name) self.tagset.remove_tag(category.name)
self.load() self.load()
return self return self
def move_to_category(self, category: Category) -> "AudioComponent": def move_to_category(self, category: Category) -> "AudioComponent":
"""Move this component to a single category, removing all others.
Args:
category: Category to move this component to exclusively.
Returns:
AudioComponent: Self for method chaining.
Raises:
NonexistentTagsetError: If tagset file doesn't exist (from Tagset.move_to_tag).
CannotParseTagsetError: If tagset file cannot be parsed (from Tagset.move_to_tag).
TagsetWriteError: If writing tagset fails (from Tagset.move_to_tag).
"""
self.tagset.move_to_tag(category.name, "user") self.tagset.move_to_tag(category.name, "user")
self.load() self.load()
return self return self
def move_to_parents(self) -> "AudioComponent": def move_to_parents(self) -> "AudioComponent":
"""Move this component to the parent categories of all current categories.
For each category this component belongs to, adds it to the parent category
and removes it from the child category.
Returns:
AudioComponent: Self for method chaining.
Raises:
NonexistentTagsetError: If tagset file doesn't exist (from Tagset operations).
CannotParseTagsetError: If tagset file cannot be parsed (from Tagset operations).
TagsetWriteError: If writing tagset fails (from Tagset operations).
KeyError: If a category tag doesn't exist during removal (from Tagset.remove_tag).
"""
for category in self.categories: for category in self.categories:
self.tagset.add_tag(category.parent.name, "user") self.tagset.add_tag(category.parent.name, "user")
self.tagset.remove_tag(category.name) self.tagset.remove_tag(category.name)

View File

@@ -1,3 +1,9 @@
"""Component bundle representation and parsing.
This module provides the Component class for loading and parsing macOS
Audio Component bundles (.component directories) and their Info.plist files.
"""
import logging import logging
import plistlib import plistlib
from dataclasses import dataclass from dataclasses import dataclass
@@ -18,6 +24,20 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class Component: class Component:
"""Represents a macOS Audio Component bundle.
A Component bundle (.component) can contain one or more AudioComponents.
This class parses the bundle's Info.plist and instantiates AudioComponent
objects for each Audio Unit defined within.
Attributes:
name: Component bundle name (without .component extension).
bundle_id: CFBundleIdentifier from Info.plist.
short_version: CFBundleShortVersionString from Info.plist.
version: CFBundleVersion from Info.plist.
audio_components: List of AudioComponent instances from this bundle.
"""
name: str name: str
bundle_id: str bundle_id: str
short_version: str short_version: str
@@ -32,6 +52,17 @@ class Component:
tags_path: Path = defaults.tags_path, tags_path: Path = defaults.tags_path,
musicapps: MusicApps = None, musicapps: MusicApps = None,
): ):
"""Initialize a Component from a bundle path.
Args:
path: Path to .component bundle (with or without .component extension).
lazy: If True, defer loading Info.plist and AudioComponents.
tags_path: Path to tags database directory.
musicapps: Shared MusicApps instance for category management.
Note:
If lazy=False, raises can occur from load() method during initialization.
"""
self.path = path if path.suffix == ".component" else Path(f"{path}.component") self.path = path if path.suffix == ".component" else Path(f"{path}.component")
self.lazy = lazy self.lazy = lazy
self.tags_path = tags_path self.tags_path = tags_path
@@ -42,6 +73,18 @@ class Component:
self.load() self.load()
def _parse_plist(self): def _parse_plist(self):
"""Parse the Info.plist file from the component bundle.
Returns:
dict: Parsed plist data.
Raises:
NonexistentPlistError: If Info.plist file doesn't exist.
CannotParsePlistError: If plist cannot be parsed. This wraps:
- plistlib.InvalidFileException: Invalid plist format.
- OSError, IOError: File read errors.
- UnicodeDecodeError: Encoding issues.
"""
info_plist_path = self.path / "Contents" / "Info.plist" info_plist_path = self.path / "Contents" / "Info.plist"
logger.debug(f"Parsing Info.plist at {info_plist_path}") logger.debug(f"Parsing Info.plist at {info_plist_path}")
if not info_plist_path.exists(): if not info_plist_path.exists():
@@ -55,6 +98,22 @@ class Component:
raise CannotParsePlistError(f"An error occurred: {e}") from e raise CannotParsePlistError(f"An error occurred: {e}") from e
def load(self) -> "Component": def load(self) -> "Component":
"""Load and parse the component bundle and its AudioComponents.
Parses Info.plist, extracts bundle metadata, and creates AudioComponent
instances for each Audio Unit defined in the AudioComponents array.
Returns:
Component: Self for method chaining.
Raises:
NonexistentPlistError: If Info.plist doesn't exist (from _parse_plist).
CannotParsePlistError: If plist parsing or metadata extraction fails.
This wraps AttributeError, TypeError from dict.get() operations.
OldComponentFormatError: If AudioComponents key is missing (legacy format).
CannotParseComponentError: If AudioComponent instantiation fails.
This wraps CannotParseComponentError from AudioComponent.__init__.
"""
plist_data = self._parse_plist() plist_data = self._parse_plist()
logger.debug(f"Loaded Info.plist for {self.path}") logger.debug(f"Loaded Info.plist for {self.path}")
@@ -95,6 +154,11 @@ class Component:
return self return self
def __hash__(self): def __hash__(self):
"""Return hash based on bundle_id for use in sets and dicts.
Returns:
int: Hash value.
"""
return hash(self.bundle_id) return hash(self.bundle_id)

View File

@@ -1,7 +1,16 @@
"""Default paths for Logic Plugin Manager.
This module provides default filesystem paths used throughout the library
for locating Audio Components and tag databases.
"""
from pathlib import Path from pathlib import Path
components_path: Path = Path("/Library/Audio/Plug-Ins/Components") components_path: Path = Path("/Library/Audio/Plug-Ins/Components")
"""Path: Default location for macOS Audio Components (.component bundles)."""
tags_path: Path = Path("~/Music/Audio Music Apps/Databases/Tags").expanduser() tags_path: Path = Path("~/Music/Audio Music Apps/Databases/Tags").expanduser()
"""Path: Default location for Logic Pro's tag and category database files."""
__all__ = ["components_path", "tags_path"] __all__ = ["components_path", "tags_path"]

View File

@@ -1,50 +1,83 @@
"""Exception classes for Logic Plugin Manager.
This module defines all custom exceptions used throughout the library for
error handling related to plugin loading, tag management, and category operations.
"""
class PluginLoadError(Exception): class PluginLoadError(Exception):
"""Base exception for errors occurring during plugin/component loading."""
pass pass
class NonexistentPlistError(PluginLoadError): class NonexistentPlistError(PluginLoadError):
"""Raised when a required Info.plist file cannot be found."""
pass pass
class CannotParsePlistError(PluginLoadError): class CannotParsePlistError(PluginLoadError):
"""Raised when a plist file exists but cannot be parsed or decoded."""
pass pass
class CannotParseComponentError(PluginLoadError): class CannotParseComponentError(PluginLoadError):
"""Raised when component data is malformed or cannot be extracted."""
pass pass
class OldComponentFormatError(PluginLoadError): class OldComponentFormatError(PluginLoadError):
"""Raised when a component uses a legacy format that is not supported."""
pass pass
class TagsetLoadError(Exception): class TagsetLoadError(Exception):
"""Base exception for errors occurring during tagset operations."""
pass pass
class NonexistentTagsetError(TagsetLoadError): class NonexistentTagsetError(TagsetLoadError):
"""Raised when a .tagset file cannot be found at the expected path."""
pass pass
class CannotParseTagsetError(TagsetLoadError): class CannotParseTagsetError(TagsetLoadError):
"""Raised when a tagset file exists but cannot be parsed."""
pass pass
class TagsetWriteError(TagsetLoadError): class TagsetWriteError(TagsetLoadError):
"""Raised when writing to a tagset file fails."""
pass pass
class MusicAppsLoadError(Exception): class MusicAppsLoadError(Exception):
"""Raised when MusicApps database files cannot be loaded or parsed."""
pass pass
class MusicAppsWriteError(Exception): class MusicAppsWriteError(Exception):
"""Raised when writing to MusicApps database files fails."""
pass pass
class CategoryValidationError(Exception): class CategoryValidationError(Exception):
"""Raised when a category name is invalid or not found in the database."""
pass pass
class CategoryExistsError(Exception): class CategoryExistsError(Exception):
"""Raised when attempting to create a category that already exists."""
pass pass

View File

@@ -1,3 +1,9 @@
"""Logic package for plugin management and search.
This package provides the main Logic interface and Plugins collection
for managing Logic Pro's audio plugins.
"""
from .logic import Logic from .logic import Logic
from .plugins import Plugins, SearchResult from .plugins import Plugins, SearchResult

View File

@@ -1,3 +1,9 @@
"""Main Logic Pro plugin management interface.
This module provides the Logic class, the primary entry point for discovering
and managing Logic Pro's audio plugins and their categorization.
"""
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -12,6 +18,20 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class Logic: class Logic:
"""Main interface for Logic Pro plugin and category management.
Provides high-level operations for discovering plugins, managing categories,
and bulk category assignments.
Attributes:
musicapps: MusicApps instance for database access.
plugins: Plugins collection with search capabilities.
components: Set of discovered Component bundles.
categories: Dictionary of category name to Category instance.
components_path: Path to Audio Components directory.
tags_path: Path to tags database directory.
"""
musicapps: MusicApps musicapps: MusicApps
plugins: Plugins plugins: Plugins
components: set[Component] components: set[Component]
@@ -26,6 +46,17 @@ class Logic:
tags_path: Path | str = None, tags_path: Path | str = None,
lazy: bool = False, lazy: bool = False,
): ):
"""Initialize Logic plugin manager.
Args:
components_path: Custom path to Components directory.
tags_path: Custom path to tags database.
lazy: If True, skip automatic discovery.
Note:
If lazy=False, automatically calls discover_plugins() and
discover_categories() which may raise various exceptions.
"""
self.components_path = ( self.components_path = (
Path(components_path) if components_path else defaults.components_path Path(components_path) if components_path else defaults.components_path
) )
@@ -48,6 +79,14 @@ class Logic:
self.discover_categories() self.discover_categories()
def discover_plugins(self) -> "Logic": def discover_plugins(self) -> "Logic":
"""Scan components directory and load all plugins.
Iterates through .component bundles, loading their AudioComponents
into the plugins collection. Failed components are logged as warnings.
Returns:
Logic: Self for method chaining.
"""
for component_path in self.components_path.glob("*.component"): for component_path in self.components_path.glob("*.component"):
try: try:
logger.debug(f"Loading component {component_path}") logger.debug(f"Loading component {component_path}")
@@ -64,6 +103,14 @@ class Logic:
return self return self
def discover_categories(self) -> "Logic": def discover_categories(self) -> "Logic":
"""Load all categories from the MusicApps database.
Returns:
Logic: Self for method chaining.
Raises:
MusicAppsLoadError: If database files cannot be loaded.
"""
for category in self.musicapps.tagpool.categories.keys(): for category in self.musicapps.tagpool.categories.keys():
logger.debug(f"Loading category {category}") logger.debug(f"Loading category {category}")
self.categories[category] = Category( self.categories[category] = Category(

View File

@@ -1,3 +1,9 @@
"""Plugin collection management and search functionality.
This module provides the Plugins class for storing, indexing, and searching
AudioComponent instances with various query strategies including fuzzy matching.
"""
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
@@ -9,12 +15,26 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class SearchResult: class SearchResult:
"""Result from a plugin search with relevance scoring.
Attributes:
plugin: The matched AudioComponent instance.
score: Relevance score (higher is better).
match_field: Field that matched (e.g., 'name', 'manufacturer').
"""
plugin: AudioComponent plugin: AudioComponent
score: float score: float
match_field: str match_field: str
class Plugins: class Plugins:
"""Collection of plugins with indexed search capabilities.
Maintains multiple indexes for fast lookups by various attributes
and provides fuzzy search functionality using rapidfuzz.
"""
def __init__(self): def __init__(self):
self._plugins: set[AudioComponent] = set() self._plugins: set[AudioComponent] = set()
@@ -29,6 +49,15 @@ class Plugins:
self._by_category: dict[str | None, set[AudioComponent]] = defaultdict(set) self._by_category: dict[str | None, set[AudioComponent]] = defaultdict(set)
def add(self, plugin: AudioComponent, *, lazy: bool = False) -> "Plugins": def add(self, plugin: AudioComponent, *, lazy: bool = False) -> "Plugins":
"""Add a plugin to the collection.
Args:
plugin: AudioComponent to add.
lazy: If True, skip indexing (call reindex_all later).
Returns:
Plugins: Self for method chaining.
"""
logger.debug(f"Adding plugin {plugin.full_name}") logger.debug(f"Adding plugin {plugin.full_name}")
self._plugins.add(plugin) self._plugins.add(plugin)
if not lazy: if not lazy:
@@ -118,6 +147,23 @@ class Plugins:
fuzzy_threshold: int = 80, fuzzy_threshold: int = 80,
max_results: int | None = None, max_results: int | None = None,
) -> list[SearchResult]: ) -> list[SearchResult]:
"""Search for plugins with scoring and fuzzy matching.
Searches across multiple fields (name, manufacturer, category, type codes)
with relevance scoring. Higher scores indicate better matches.
Args:
query: Search query string.
use_fuzzy: Enable fuzzy matching (requires rapidfuzz package).
fuzzy_threshold: Minimum fuzzy match score (0-100).
max_results: Limit number of results (None for unlimited).
Returns:
list[SearchResult]: Sorted search results (highest score first).
Raises:
ImportError: If use_fuzzy=True but rapidfuzz is not installed.
"""
if not query or not query.strip(): if not query or not query.strip():
return [] return []

View File

@@ -1,3 +1,9 @@
"""Tags package for category and metadata management.
This package provides classes for managing Logic Pro's tag database system,
including categories, plugin metadata, and sorting preferences.
"""
from .category import Category from .category import Category
from .musicapps import MusicApps, Properties, Tagpool from .musicapps import MusicApps, Properties, Tagpool
from .tagset import Tagset from .tagset import Tagset

View File

@@ -1,3 +1,9 @@
"""Category management for plugin organization.
This module provides the Category class for working with Logic Pro's hierarchical
category system used to organize and filter audio plugins.
"""
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -9,6 +15,19 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class Category: class Category:
"""Represents a Logic Pro plugin category.
Categories form a hierarchical structure using colon-separated names
(e.g., 'Effects:EQ'). Each category tracks plugin count and sorting order.
Attributes:
name: Category name (colon-separated for hierarchy).
musicapps: MusicApps instance for database access.
is_root: True if this is the root category (empty name).
plugin_amount: Number of plugins in this category.
lazy: Whether lazy loading is enabled.
"""
name: str name: str
musicapps: MusicApps = field(repr=False) musicapps: MusicApps = field(repr=False)
is_root: bool is_root: bool
@@ -16,6 +35,16 @@ class Category:
lazy: bool lazy: bool
def __init__(self, name: str, *, musicapps: MusicApps = None, lazy: bool = False): def __init__(self, name: str, *, musicapps: MusicApps = None, lazy: bool = False):
"""Initialize a Category.
Args:
name: Category name.
musicapps: MusicApps instance (created if None).
lazy: If True, defer validation.
Note:
If lazy=False, raises can occur from load() during initialization.
"""
self.name = name self.name = name
self.musicapps = musicapps or MusicApps(lazy=lazy) self.musicapps = musicapps or MusicApps(lazy=lazy)
self.is_root = False self.is_root = False
@@ -26,6 +55,12 @@ class Category:
self.load() self.load()
def load(self): def load(self):
"""Validate and load category data from MusicApps database.
Raises:
CategoryValidationError: If category doesn't exist in database.
MusicAppsLoadError: If database files cannot be loaded.
"""
logger.debug(f"Validating category {self.name}") logger.debug(f"Validating category {self.name}")
if self.name not in self.musicapps.tagpool.categories.keys(): if self.name not in self.musicapps.tagpool.categories.keys():
raise CategoryValidationError(f"Category {self.name} not found in tagpool") raise CategoryValidationError(f"Category {self.name} not found in tagpool")
@@ -41,6 +76,21 @@ class Category:
@classmethod @classmethod
def introduce(cls, name: str, *, musicapps: MusicApps = None, lazy: bool = False): def introduce(cls, name: str, *, musicapps: MusicApps = None, lazy: bool = False):
"""Create a new category in the database.
Args:
name: Name for the new category.
musicapps: MusicApps instance (created if None).
lazy: Whether to use lazy loading.
Returns:
Category: Newly created category instance.
Raises:
CategoryExistsError: If category already exists.
MusicAppsLoadError: If database files cannot be loaded.
MusicAppsWriteError: If database files cannot be written.
"""
logger.debug(f"Introducing category {name}") logger.debug(f"Introducing category {name}")
if musicapps is None: if musicapps is None:
musicapps = MusicApps() musicapps = MusicApps()
@@ -58,6 +108,11 @@ class Category:
@property @property
def parent(self) -> "Category": def parent(self) -> "Category":
"""Get the parent category in the hierarchy.
Returns:
Category: Parent category, or self if this is root.
"""
if self.is_root: if self.is_root:
return self return self
return self.__class__( return self.__class__(
@@ -67,6 +122,14 @@ class Category:
) )
def child(self, name: str) -> "Category": def child(self, name: str) -> "Category":
"""Create a child category reference.
Args:
name: Child category name (without parent prefix).
Returns:
Category: Child category instance.
"""
return self.__class__( return self.__class__(
f"{self.name}:{name}", musicapps=self.musicapps, lazy=self.lazy f"{self.name}:{name}", musicapps=self.musicapps, lazy=self.lazy
) )

View File

@@ -1,3 +1,10 @@
"""MusicApps database management for Logic Pro tags and categories.
This module provides classes for reading and writing Logic Pro's MusicApps database
files (MusicApps.tagpool and MusicApps.properties) which store category definitions,
plugin counts, and sorting information.
"""
import logging import logging
import plistlib import plistlib
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -10,6 +17,18 @@ logger = logging.getLogger(__name__)
def _parse_plist(path: Path): def _parse_plist(path: Path):
"""Parse a plist file from the MusicApps database.
Args:
path: Path to plist file.
Returns:
dict: Parsed plist data.
Raises:
MusicAppsLoadError: If file doesn't exist or cannot be parsed.
This wraps plistlib.InvalidFileException, OSError, IOError, UnicodeDecodeError.
"""
logger.debug(f"Parsing plist at {path}") logger.debug(f"Parsing plist at {path}")
if not path.exists(): if not path.exists():
raise MusicAppsLoadError(f"File not found at {path}") raise MusicAppsLoadError(f"File not found at {path}")
@@ -23,6 +42,16 @@ def _parse_plist(path: Path):
def _save_plist(path: Path, data: dict): def _save_plist(path: Path, data: dict):
"""Save data to a plist file in the MusicApps database.
Args:
path: Path to plist file.
data: Dictionary to serialize and write.
Raises:
MusicAppsWriteError: If writing fails.
This wraps OSError, IOError, TypeError.
"""
logger.debug(f"Saving plist to {path}") logger.debug(f"Saving plist to {path}")
try: try:
with open(path, "wb") as fp: with open(path, "wb") as fp:
@@ -34,9 +63,24 @@ def _save_plist(path: Path, data: dict):
@dataclass @dataclass
class Tagpool: class Tagpool:
"""Represents MusicApps.tagpool - category to plugin count mapping.
The tagpool file stores a dictionary mapping category names to the number
of plugins assigned to each category.
Attributes:
categories: Dictionary mapping category names to plugin counts.
"""
categories: dict[str, int] categories: dict[str, int]
def __init__(self, tags_path: Path, *, lazy: bool = False): def __init__(self, tags_path: Path, *, lazy: bool = False):
"""Initialize Tagpool from database path.
Args:
tags_path: Path to tags database directory.
lazy: If True, defer loading the file.
"""
self.path = tags_path / "MusicApps.tagpool" self.path = tags_path / "MusicApps.tagpool"
self.lazy = lazy self.lazy = lazy
@@ -46,24 +90,60 @@ class Tagpool:
self.load() self.load()
def load(self) -> "Tagpool": def load(self) -> "Tagpool":
"""Load tagpool data from disk.
Returns:
Tagpool: Self for method chaining.
Raises:
MusicAppsLoadError: If file cannot be loaded (from _parse_plist).
"""
logger.debug(f"Loading Tagpool data from {self.path}") logger.debug(f"Loading Tagpool data from {self.path}")
self.categories = _parse_plist(self.path) self.categories = _parse_plist(self.path)
logger.debug(f"Loaded Tagpool data from {self.path}") logger.debug(f"Loaded Tagpool data from {self.path}")
return self return self
def write_category(self, name: str, plugin_count: int = 0): def write_category(self, name: str, plugin_count: int = 0):
"""Write or update a category with its plugin count.
Args:
name: Category name.
plugin_count: Number of plugins in this category.
Raises:
MusicAppsLoadError: If file cannot be loaded (from load).
MusicAppsWriteError: If file cannot be written (from _save_plist).
"""
self.load() self.load()
self.categories[name] = plugin_count self.categories[name] = plugin_count
_save_plist(self.path, self.categories) _save_plist(self.path, self.categories)
self.load() self.load()
def introduce_category(self, name: str): def introduce_category(self, name: str):
"""Add a new category if it doesn't exist.
Args:
name: Category name to introduce.
Raises:
MusicAppsLoadError: If file cannot be loaded (from load).
MusicAppsWriteError: If file cannot be written (from write_category).
"""
self.load() self.load()
if name in self.categories: if name in self.categories:
return return
self.write_category(name) self.write_category(name)
def remove_category(self, name: str): def remove_category(self, name: str):
"""Remove a category from the tagpool.
Args:
name: Category name to remove.
Raises:
MusicAppsLoadError: If file cannot be loaded (from load).
MusicAppsWriteError: If file cannot be written (from _save_plist).
"""
self.load() self.load()
self.categories.pop(name, None) self.categories.pop(name, None)
_save_plist(self.path, self.categories) _save_plist(self.path, self.categories)
@@ -72,11 +152,27 @@ class Tagpool:
@dataclass @dataclass
class Properties: class Properties:
"""Represents MusicApps.properties - category sorting and preferences.
The properties file stores the category sorting order and whether user
sorting is enabled (vs. alphabetical sorting).
Attributes:
sorting: Ordered list of category names.
user_sorted: True if user sorting is enabled, False for alphabetical.
"""
sorting: list[str] sorting: list[str]
user_sorted: bool user_sorted: bool
__raw_data: dict[str, str | list[str] | bool] = field(repr=False) __raw_data: dict[str, str | list[str] | bool] = field(repr=False)
def __init__(self, tags_path: Path, *, lazy: bool = False): def __init__(self, tags_path: Path, *, lazy: bool = False):
"""Initialize Properties from database path.
Args:
tags_path: Path to tags database directory.
lazy: If True, defer loading the file.
"""
self.path = tags_path / "MusicApps.properties" self.path = tags_path / "MusicApps.properties"
self.lazy = lazy self.lazy = lazy
@@ -86,6 +182,14 @@ class Properties:
self.load() self.load()
def load(self) -> "Properties": def load(self) -> "Properties":
"""Load properties data from disk.
Returns:
Properties: Self for method chaining.
Raises:
MusicAppsLoadError: If file cannot be loaded (from _parse_plist).
"""
logger.debug(f"Loading Properties data from {self.path}") logger.debug(f"Loading Properties data from {self.path}")
self.__raw_data = _parse_plist(self.path) self.__raw_data = _parse_plist(self.path)
logger.debug(f"Loaded Properties data from {self.path}") logger.debug(f"Loaded Properties data from {self.path}")
@@ -122,6 +226,17 @@ class Properties:
self.load() self.load()
def move_up(self, category: str, steps: int = 1): def move_up(self, category: str, steps: int = 1):
"""Move a category up in the sorting order.
Args:
category: Category name to move.
steps: Number of positions to move up.
Raises:
ValueError: If category not found in sorting.
MusicAppsLoadError: If file cannot be loaded (from load).
MusicAppsWriteError: If file cannot be written (from _save_plist).
"""
self.load() self.load()
sorting = self.sorting.copy() sorting = self.sorting.copy()
@@ -300,10 +415,26 @@ class Properties:
@dataclass @dataclass
class MusicApps: class MusicApps:
"""Main interface to Logic Pro's MusicApps database.
Provides unified access to both tagpool and properties files, managing
category definitions, plugin counts, and sorting preferences.
Attributes:
tagpool: Tagpool instance managing category/plugin counts.
properties: Properties instance managing sorting and preferences.
"""
tagpool: Tagpool tagpool: Tagpool
properties: Properties properties: Properties
def __init__(self, tags_path: Path = defaults.tags_path, *, lazy: bool = False): def __init__(self, tags_path: Path = defaults.tags_path, *, lazy: bool = False):
"""Initialize MusicApps from database path.
Args:
tags_path: Path to tags database directory.
lazy: If True, defer loading files.
"""
self.path = tags_path self.path = tags_path
self.lazy = lazy self.lazy = lazy
@@ -313,16 +444,42 @@ class MusicApps:
self.load() self.load()
def load(self) -> "MusicApps": def load(self) -> "MusicApps":
"""Load both tagpool and properties files.
Returns:
MusicApps: Self for method chaining.
Raises:
MusicAppsLoadError: If files cannot be loaded (from Tagpool/Properties).
"""
self.tagpool = Tagpool(self.path, lazy=self.lazy) self.tagpool = Tagpool(self.path, lazy=self.lazy)
self.properties = Properties(self.path, lazy=self.lazy) self.properties = Properties(self.path, lazy=self.lazy)
logger.debug(f"Loaded MusicApps from {self.path}") logger.debug(f"Loaded MusicApps from {self.path}")
return self return self
def introduce_category(self, name: str): def introduce_category(self, name: str):
"""Add a new category to both tagpool and properties.
Args:
name: Category name to introduce.
Raises:
MusicAppsLoadError: If files cannot be loaded.
MusicAppsWriteError: If files cannot be written.
"""
self.tagpool.introduce_category(name) self.tagpool.introduce_category(name)
self.properties.introduce_category(name) self.properties.introduce_category(name)
def remove_category(self, name: str): def remove_category(self, name: str):
"""Remove a category from both tagpool and properties.
Args:
name: Category name to remove.
Raises:
MusicAppsLoadError: If files cannot be loaded.
MusicAppsWriteError: If files cannot be written.
"""
self.tagpool.remove_category(name) self.tagpool.remove_category(name)
self.properties.remove_category(name) self.properties.remove_category(name)

View File

@@ -1,3 +1,9 @@
"""Tagset file management for Audio Components.
This module provides the Tagset class for reading and writing .tagset files
that store plugin metadata like nicknames, short names, and category tags.
"""
import plistlib import plistlib
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
@@ -11,6 +17,19 @@ from ..exceptions import (
@dataclass @dataclass
class Tagset: class Tagset:
"""Represents a .tagset file containing plugin metadata and tags.
Tagset files store custom metadata and category tags for Audio Components.
Each tagset is identified by a unique tags_id derived from the component's
type, subtype, and manufacturer codes.
Attributes:
tags_id: Unique identifier (hex-encoded type-subtype-manufacturer).
nickname: Custom nickname for the plugin.
shortname: Custom short name for the plugin.
tags: Dictionary mapping category names to tag values (e.g., 'user').
"""
tags_id: str tags_id: str
nickname: str nickname: str
shortname: str shortname: str
@@ -18,6 +37,15 @@ class Tagset:
__raw_data: dict[str, str | dict[str, str]] = field(repr=False) __raw_data: dict[str, str | dict[str, str]] = field(repr=False)
def __init__(self, path: Path, *, lazy: bool = False): def __init__(self, path: Path, *, lazy: bool = False):
"""Initialize a Tagset from a file path.
Args:
path: Path to .tagset file (extension added automatically if missing).
lazy: If True, defer loading the file until needed.
Note:
If lazy=False, raises can occur from load() method during initialization.
"""
self.path = path.with_suffix(".tagset") self.path = path.with_suffix(".tagset")
self.lazy = lazy self.lazy = lazy
@@ -25,6 +53,18 @@ class Tagset:
self.load() self.load()
def _parse_plist(self): def _parse_plist(self):
"""Parse the .tagset plist file.
Returns:
dict: Parsed plist data.
Raises:
NonexistentTagsetError: If .tagset file doesn't exist.
CannotParseTagsetError: If plist cannot be parsed. This wraps:
- plistlib.InvalidFileException: Invalid plist format.
- OSError, IOError: File read errors.
- UnicodeDecodeError: Encoding issues.
"""
if not self.path.exists(): if not self.path.exists():
raise NonexistentTagsetError(f".tagset not found at {self.path}") raise NonexistentTagsetError(f".tagset not found at {self.path}")
try: try:
@@ -35,6 +75,13 @@ class Tagset:
raise CannotParseTagsetError(f"An error occurred: {e}") from e raise CannotParseTagsetError(f"An error occurred: {e}") from e
def _write_plist(self): def _write_plist(self):
"""Write the tagset data to the .tagset plist file.
Raises:
TagsetWriteError: If writing fails. This wraps:
- OSError, IOError: File write errors.
- TypeError: If data contains non-serializable types.
"""
try: try:
with open(self.path, "wb") as fp: with open(self.path, "wb") as fp:
plistlib.dump(self.__raw_data, fp) plistlib.dump(self.__raw_data, fp)
@@ -42,6 +89,15 @@ class Tagset:
raise TagsetWriteError(f"An error occurred: {e}") from e raise TagsetWriteError(f"An error occurred: {e}") from e
def load(self) -> "Tagset": def load(self) -> "Tagset":
"""Load and parse the tagset file from disk.
Returns:
Tagset: Self for method chaining.
Raises:
NonexistentTagsetError: If .tagset file doesn't exist (from _parse_plist).
CannotParseTagsetError: If plist cannot be parsed (from _parse_plist).
"""
self.__raw_data = self._parse_plist() self.__raw_data = self._parse_plist()
self.tags_id = self.path.name.removesuffix(".tagset") self.tags_id = self.path.name.removesuffix(".tagset")
@@ -52,36 +108,99 @@ class Tagset:
return self return self
def set_nickname(self, nickname: str): def set_nickname(self, nickname: str):
"""Set the nickname field in the tagset.
Args:
nickname: New nickname value.
Raises:
NonexistentTagsetError: If .tagset file doesn't exist (from load).
CannotParseTagsetError: If plist cannot be parsed (from load).
TagsetWriteError: If writing fails (from _write_plist).
"""
self.load() self.load()
self.__raw_data["nickname"] = nickname self.__raw_data["nickname"] = nickname
self._write_plist() self._write_plist()
self.load() self.load()
def set_shortname(self, shortname: str): def set_shortname(self, shortname: str):
"""Set the shortname field in the tagset.
Args:
shortname: New short name value.
Raises:
NonexistentTagsetError: If .tagset file doesn't exist (from load).
CannotParseTagsetError: If plist cannot be parsed (from load).
TagsetWriteError: If writing fails (from _write_plist).
"""
self.load() self.load()
self.__raw_data["shortname"] = shortname self.__raw_data["shortname"] = shortname
self._write_plist() self._write_plist()
self.load() self.load()
def set_tags(self, tags: dict[str, str]): def set_tags(self, tags: dict[str, str]):
"""Replace all tags with the provided dictionary.
Args:
tags: Dictionary mapping category names to tag values.
Raises:
NonexistentTagsetError: If .tagset file doesn't exist (from load).
CannotParseTagsetError: If plist cannot be parsed (from load).
TagsetWriteError: If writing fails (from _write_plist).
"""
self.load() self.load()
self.__raw_data["tags"] = tags self.__raw_data["tags"] = tags
self._write_plist() self._write_plist()
self.load() self.load()
def add_tag(self, tag: str, value: str): def add_tag(self, tag: str, value: str):
"""Add or update a single tag.
Args:
tag: Category name.
value: Tag value (typically 'user').
Raises:
NonexistentTagsetError: If .tagset file doesn't exist (from load).
CannotParseTagsetError: If plist cannot be parsed (from load).
TagsetWriteError: If writing fails (from _write_plist).
"""
self.load() self.load()
self.tags[tag] = value self.tags[tag] = value
self._write_plist() self._write_plist()
self.load() self.load()
def remove_tag(self, tag: str): def remove_tag(self, tag: str):
"""Remove a tag from the tagset.
Args:
tag: Category name to remove.
Raises:
NonexistentTagsetError: If .tagset file doesn't exist (from load).
CannotParseTagsetError: If plist cannot be parsed (from load).
KeyError: If tag doesn't exist in the tagset.
TagsetWriteError: If writing fails (from _write_plist).
"""
self.load() self.load()
del self.tags[tag] del self.tags[tag]
self._write_plist() self._write_plist()
self.load() self.load()
def move_to_tag(self, tag: str, value: str): def move_to_tag(self, tag: str, value: str):
"""Clear all tags and set a single tag.
Args:
tag: Category name.
value: Tag value (typically 'user').
Raises:
NonexistentTagsetError: If .tagset file doesn't exist (from load).
CannotParseTagsetError: If plist cannot be parsed (from load).
TagsetWriteError: If writing fails (from _write_plist).
"""
self.load() self.load()
self.tags.clear() self.tags.clear()
self.tags[tag] = value self.tags[tag] = value

392
uv.lock generated
View File

@@ -2,9 +2,166 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]]
name = "accessible-pygments"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" },
]
[[package]]
name = "alabaster"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
]
[[package]]
name = "babel"
version = "2.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "docutils"
version = "0.21.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
]
[[package]]
name = "furo"
version = "2025.9.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "accessible-pygments" },
{ name = "beautifulsoup4" },
{ name = "pygments" },
{ name = "sphinx" },
{ name = "sphinx-basic-ng" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/29/ff3b83a1ffce74676043ab3e7540d398e0b1ce7660917a00d7c4958b93da/furo-2025.9.25.tar.gz", hash = "sha256:3eac05582768fdbbc2bdfa1cdbcdd5d33cfc8b4bd2051729ff4e026a1d7e0a98", size = 1662007, upload-time = "2025-09-25T21:37:19.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/69/964b55f389c289e16ba2a5dfe587c3c462aac09e24123f09ddf703889584/furo-2025.9.25-py3-none-any.whl", hash = "sha256:2937f68e823b8e37b410c972c371bc2b1d88026709534927158e0cb3fac95afe", size = 340409, upload-time = "2025-09-25T21:37:17.244Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "imagesize"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]] [[package]]
name = "logic-plugin-manager" name = "logic-plugin-manager"
version = "0.1.0" version = "0.0.1"
source = { editable = "." } source = { editable = "." }
[package.optional-dependencies] [package.optional-dependencies]
@@ -14,8 +171,11 @@ search = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "furo" },
{ name = "rapidfuzz" }, { name = "rapidfuzz" },
{ name = "rich" }, { name = "rich" },
{ name = "sphinx" },
{ name = "sphinx-autodoc-typehints" },
] ]
[package.metadata] [package.metadata]
@@ -24,8 +184,11 @@ provides-extras = ["search"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "furo", specifier = ">=2025.9.25" },
{ name = "rapidfuzz", specifier = ">=3.14.3" }, { name = "rapidfuzz", specifier = ">=3.14.3" },
{ name = "rich", specifier = ">=14.2.0" }, { name = "rich", specifier = ">=14.2.0" },
{ name = "sphinx", specifier = ">=8.2.3" },
{ name = "sphinx-autodoc-typehints", specifier = ">=3.5.2" },
] ]
[[package]] [[package]]
@@ -40,6 +203,58 @@ 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" }, { 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 = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]] [[package]]
name = "mdurl" name = "mdurl"
version = "0.1.2" version = "0.1.2"
@@ -49,6 +264,15 @@ 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" }, { 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 = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.19.2" version = "2.19.2"
@@ -110,6 +334,21 @@ wheels = [
{ 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" }, { 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 = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]] [[package]]
name = "rich" name = "rich"
version = "14.2.0" version = "14.2.0"
@@ -122,3 +361,154 @@ sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7b
wheels = [ 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" }, { 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" },
] ]
[[package]]
name = "roman-numerals-py"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" },
]
[[package]]
name = "snowballstemmer"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
]
[[package]]
name = "soupsieve"
version = "2.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
]
[[package]]
name = "sphinx"
version = "8.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "alabaster" },
{ name = "babel" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils" },
{ name = "imagesize" },
{ name = "jinja2" },
{ name = "packaging" },
{ name = "pygments" },
{ name = "requests" },
{ name = "roman-numerals-py" },
{ name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" },
]
[[package]]
name = "sphinx-autodoc-typehints"
version = "3.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/4f/4fd5583678bb7dc8afa69e9b309e6a99ee8d79ad3a4728f4e52fd7cb37c7/sphinx_autodoc_typehints-3.5.2.tar.gz", hash = "sha256:5fcd4a3eb7aa89424c1e2e32bedca66edc38367569c9169a80f4b3e934171fdb", size = 37839, upload-time = "2025-10-16T00:50:15.743Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/f2/9657c98a66973b7c35bfd48ba65d1922860de9598fbb535cd96e3f58a908/sphinx_autodoc_typehints-3.5.2-py3-none-any.whl", hash = "sha256:0accd043619f53c86705958e323b419e41667917045ac9215d7be1b493648d8c", size = 21184, upload-time = "2025-10-16T00:50:13.973Z" },
]
[[package]]
name = "sphinx-basic-ng"
version = "1.0.0b2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sphinx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" },
]
[[package]]
name = "sphinxcontrib-applehelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
]
[[package]]
name = "sphinxcontrib-devhelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
]
[[package]]
name = "sphinxcontrib-qthelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]