From af227b597b6bf52383cae70f372ef344dfba99d9 Mon Sep 17 00:00:00 2001 From: h Date: Fri, 7 Nov 2025 17:15:12 +0100 Subject: [PATCH] docs(global): add documentation --- .gitignore | 5 + .readthedocs.yaml | 22 + README.md | 73 +- docs/Makefile | 20 + docs/conf.py | 58 ++ docs/core_concepts.rst | 467 ++++++++++++ docs/examples.rst | 712 ++++++++++++++++++ docs/getting_started.rst | 240 ++++++ docs/index.rst | 91 +++ docs/logic_plugin_manager.components.rst | 20 + docs/logic_plugin_manager.logic.rst | 20 + docs/logic_plugin_manager.rst | 28 + docs/logic_plugin_manager.tags.rst | 28 + docs/make.bat | 35 + docs/requirements.txt | 3 + pyproject.toml | 3 + src/logic_plugin_manager/__init__.py | 20 + .../components/__init__.py | 6 + .../components/audiocomponent.py | 197 +++++ .../components/component.py | 64 ++ src/logic_plugin_manager/defaults.py | 9 + src/logic_plugin_manager/exceptions.py | 33 + src/logic_plugin_manager/logic/__init__.py | 6 + src/logic_plugin_manager/logic/logic.py | 47 ++ src/logic_plugin_manager/logic/plugins.py | 46 ++ src/logic_plugin_manager/tags/__init__.py | 6 + src/logic_plugin_manager/tags/category.py | 63 ++ src/logic_plugin_manager/tags/musicapps.py | 157 ++++ src/logic_plugin_manager/tags/tagset.py | 119 +++ uv.lock | 392 +++++++++- 30 files changed, 2978 insertions(+), 12 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/core_concepts.rst create mode 100644 docs/examples.rst create mode 100644 docs/getting_started.rst create mode 100644 docs/index.rst create mode 100644 docs/logic_plugin_manager.components.rst create mode 100644 docs/logic_plugin_manager.logic.rst create mode 100644 docs/logic_plugin_manager.rst create mode 100644 docs/logic_plugin_manager.tags.rst create mode 100644 docs/make.bat create mode 100644 docs/requirements.txt diff --git a/.gitignore b/.gitignore index cfcc61e..f543eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ wheels/ # Development and testing .idea t + +# Docs +docs/_build/ +docs/_static/ +docs/_templates/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..34a7e6d --- /dev/null +++ b/.readthedocs.yaml @@ -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 diff --git a/README.md b/README.md index 80ca425..ec4bd4c 100644 --- a/README.md +++ b/README.md @@ -3,32 +3,83 @@ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE.md) [![Commercial License](https://img.shields.io/badge/License-Commercial-green.svg)](LICENSE-COMMERCIAL.md) [![PyPI version](https://badge.fury.io/py/logic-plugin-manager.svg)](https://pypi.org/project/logic-plugin-manager/) +[![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 -uv add logic-plugin-manager +pip install logic-plugin-manager ``` -### With search functionality (includes rapidfuzz): +For fuzzy search functionality: ```bash -uv add logic-plugin-manager[search] +pip install logic-plugin-manager[search] ``` -## 🚀 Quick Start +## Usage + ```python from logic_plugin_manager import Logic +# Initialize and discover plugins 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: -#### 🆓 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). + +**Open Source (AGPL-3.0)**: Free for open source projects. See [LICENSE.md](LICENSE.md). + +**Commercial License**: Required for closed-source or commercial applications. See [LICENSE-COMMERCIAL.md](LICENSE-COMMERCIAL.md). 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 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -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) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..e6886ef --- /dev/null +++ b/docs/conf.py @@ -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, +} diff --git a/docs/core_concepts.rst b/docs/core_concepts.rst new file mode 100644 index 0000000..200f4d4 --- /dev/null +++ b/docs/core_concepts.rst @@ -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/.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) diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..8cb9b85 --- /dev/null +++ b/docs/examples.rst @@ -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") diff --git a/docs/getting_started.rst b/docs/getting_started.rst new file mode 100644 index 0000000..78f5fe6 --- /dev/null +++ b/docs/getting_started.rst @@ -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. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..bcd6807 --- /dev/null +++ b/docs/index.rst @@ -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 + diff --git a/docs/logic_plugin_manager.components.rst b/docs/logic_plugin_manager.components.rst new file mode 100644 index 0000000..7d60621 --- /dev/null +++ b/docs/logic_plugin_manager.components.rst @@ -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: diff --git a/docs/logic_plugin_manager.logic.rst b/docs/logic_plugin_manager.logic.rst new file mode 100644 index 0000000..e1023eb --- /dev/null +++ b/docs/logic_plugin_manager.logic.rst @@ -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: diff --git a/docs/logic_plugin_manager.rst b/docs/logic_plugin_manager.rst new file mode 100644 index 0000000..35723c6 --- /dev/null +++ b/docs/logic_plugin_manager.rst @@ -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: diff --git a/docs/logic_plugin_manager.tags.rst b/docs/logic_plugin_manager.tags.rst new file mode 100644 index 0000000..4033122 --- /dev/null +++ b/docs/logic_plugin_manager.tags.rst @@ -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: diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/docs/make.bat @@ -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 diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..fb25b93 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx +furo +sphinx-autodoc-typehints diff --git a/pyproject.toml b/pyproject.toml index 7d62036..7583a7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,9 @@ build-backend = "uv_build" dev = [ "rich>=14.2.0", "rapidfuzz>=3.14.3", + "sphinx>=8.2.3", + "sphinx-autodoc-typehints>=3.5.2", + "furo>=2025.9.25", ] [tool.isort] diff --git a/src/logic_plugin_manager/__init__.py b/src/logic_plugin_manager/__init__.py index 8674ee7..2245616 100644 --- a/src/logic_plugin_manager/__init__.py +++ b/src/logic_plugin_manager/__init__.py @@ -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 from .components import AudioComponent, AudioUnitType, Component diff --git a/src/logic_plugin_manager/components/__init__.py b/src/logic_plugin_manager/components/__init__.py index ed9b849..9ae3de7 100644 --- a/src/logic_plugin_manager/components/__init__.py +++ b/src/logic_plugin_manager/components/__init__.py @@ -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 .component import Component diff --git a/src/logic_plugin_manager/components/audiocomponent.py b/src/logic_plugin_manager/components/audiocomponent.py index 50e8ac1..cd6dc14 100644 --- a/src/logic_plugin_manager/components/audiocomponent.py +++ b/src/logic_plugin_manager/components/audiocomponent.py @@ -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 from dataclasses import dataclass, field from enum import Enum @@ -11,6 +17,11 @@ logger = logging.getLogger(__name__) 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") AUMU = ("aumu", "Instrument", "Music Device") AUMF = ("aumf", "MIDI-controlled Effects", "Music Effect") @@ -19,18 +30,41 @@ class AudioUnitType(Enum): @property 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] @property 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] @property 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] @classmethod 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() for unit_type in cls: if unit_type.code == code_lower: @@ -39,6 +73,16 @@ class AudioUnitType(Enum): @classmethod 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() results = [] for unit_type in cls: @@ -53,6 +97,27 @@ class AudioUnitType(Enum): @dataclass 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 manufacturer: str name: str @@ -75,6 +140,19 @@ class AudioComponent: tags_path: Path = defaults.tags_path, 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.lazy = lazy self.musicapps = musicapps or MusicApps(tags_path=self.tags_path, lazy=lazy) @@ -105,6 +183,19 @@ class AudioComponent: self.load() 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}") self.tagset = Tagset(self.tags_path / self.tags_id, lazy=self.lazy) logger.debug(f"Loaded Tagset for {self.full_name}") @@ -123,44 +214,150 @@ class AudioComponent: return self 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): return NotImplemented return self.tags_id == other.tags_id def __hash__(self): + """Return hash based on tags_id for use in sets and dicts. + + Returns: + int: Hash value. + """ return hash(self.tags_id) 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.load() return self 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.load() return self 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.load() return self 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.load() return self 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.load() return self 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.load() return self 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: self.tagset.add_tag(category.parent.name, "user") self.tagset.remove_tag(category.name) diff --git a/src/logic_plugin_manager/components/component.py b/src/logic_plugin_manager/components/component.py index b6df97a..571f592 100644 --- a/src/logic_plugin_manager/components/component.py +++ b/src/logic_plugin_manager/components/component.py @@ -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 plistlib from dataclasses import dataclass @@ -18,6 +24,20 @@ logger = logging.getLogger(__name__) @dataclass 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 bundle_id: str short_version: str @@ -32,6 +52,17 @@ class Component: tags_path: Path = defaults.tags_path, 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.lazy = lazy self.tags_path = tags_path @@ -42,6 +73,18 @@ class Component: self.load() 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" logger.debug(f"Parsing Info.plist at {info_plist_path}") if not info_plist_path.exists(): @@ -55,6 +98,22 @@ class Component: raise CannotParsePlistError(f"An error occurred: {e}") from e 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() logger.debug(f"Loaded Info.plist for {self.path}") @@ -95,6 +154,11 @@ class Component: return 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) diff --git a/src/logic_plugin_manager/defaults.py b/src/logic_plugin_manager/defaults.py index 485e68b..06ed7b3 100644 --- a/src/logic_plugin_manager/defaults.py +++ b/src/logic_plugin_manager/defaults.py @@ -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 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() +"""Path: Default location for Logic Pro's tag and category database files.""" __all__ = ["components_path", "tags_path"] diff --git a/src/logic_plugin_manager/exceptions.py b/src/logic_plugin_manager/exceptions.py index dbceabe..c19763a 100644 --- a/src/logic_plugin_manager/exceptions.py +++ b/src/logic_plugin_manager/exceptions.py @@ -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): + """Base exception for errors occurring during plugin/component loading.""" + pass class NonexistentPlistError(PluginLoadError): + """Raised when a required Info.plist file cannot be found.""" + pass class CannotParsePlistError(PluginLoadError): + """Raised when a plist file exists but cannot be parsed or decoded.""" + pass class CannotParseComponentError(PluginLoadError): + """Raised when component data is malformed or cannot be extracted.""" + pass class OldComponentFormatError(PluginLoadError): + """Raised when a component uses a legacy format that is not supported.""" + pass class TagsetLoadError(Exception): + """Base exception for errors occurring during tagset operations.""" + pass class NonexistentTagsetError(TagsetLoadError): + """Raised when a .tagset file cannot be found at the expected path.""" + pass class CannotParseTagsetError(TagsetLoadError): + """Raised when a tagset file exists but cannot be parsed.""" + pass class TagsetWriteError(TagsetLoadError): + """Raised when writing to a tagset file fails.""" + pass class MusicAppsLoadError(Exception): + """Raised when MusicApps database files cannot be loaded or parsed.""" + pass class MusicAppsWriteError(Exception): + """Raised when writing to MusicApps database files fails.""" + pass class CategoryValidationError(Exception): + """Raised when a category name is invalid or not found in the database.""" + pass class CategoryExistsError(Exception): + """Raised when attempting to create a category that already exists.""" + pass diff --git a/src/logic_plugin_manager/logic/__init__.py b/src/logic_plugin_manager/logic/__init__.py index 82a379d..5b13a48 100644 --- a/src/logic_plugin_manager/logic/__init__.py +++ b/src/logic_plugin_manager/logic/__init__.py @@ -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 .plugins import Plugins, SearchResult diff --git a/src/logic_plugin_manager/logic/logic.py b/src/logic_plugin_manager/logic/logic.py index 9c23013..9164045 100644 --- a/src/logic_plugin_manager/logic/logic.py +++ b/src/logic_plugin_manager/logic/logic.py @@ -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 from dataclasses import dataclass from pathlib import Path @@ -12,6 +18,20 @@ logger = logging.getLogger(__name__) @dataclass 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 plugins: Plugins components: set[Component] @@ -26,6 +46,17 @@ class Logic: tags_path: Path | str = None, 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 = ( Path(components_path) if components_path else defaults.components_path ) @@ -48,6 +79,14 @@ class Logic: self.discover_categories() 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"): try: logger.debug(f"Loading component {component_path}") @@ -64,6 +103,14 @@ class Logic: return self 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(): logger.debug(f"Loading category {category}") self.categories[category] = Category( diff --git a/src/logic_plugin_manager/logic/plugins.py b/src/logic_plugin_manager/logic/plugins.py index f98dff0..0eb19b9 100644 --- a/src/logic_plugin_manager/logic/plugins.py +++ b/src/logic_plugin_manager/logic/plugins.py @@ -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 from collections import defaultdict from dataclasses import dataclass @@ -9,12 +15,26 @@ logger = logging.getLogger(__name__) @dataclass 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 score: float match_field: str 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): self._plugins: set[AudioComponent] = set() @@ -29,6 +49,15 @@ class Plugins: self._by_category: dict[str | None, set[AudioComponent]] = defaultdict(set) 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}") self._plugins.add(plugin) if not lazy: @@ -118,6 +147,23 @@ class Plugins: fuzzy_threshold: int = 80, max_results: int | None = None, ) -> 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(): return [] diff --git a/src/logic_plugin_manager/tags/__init__.py b/src/logic_plugin_manager/tags/__init__.py index 21cc00b..3e399ef 100644 --- a/src/logic_plugin_manager/tags/__init__.py +++ b/src/logic_plugin_manager/tags/__init__.py @@ -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 .musicapps import MusicApps, Properties, Tagpool from .tagset import Tagset diff --git a/src/logic_plugin_manager/tags/category.py b/src/logic_plugin_manager/tags/category.py index 6634b8c..797292e 100644 --- a/src/logic_plugin_manager/tags/category.py +++ b/src/logic_plugin_manager/tags/category.py @@ -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 from dataclasses import dataclass, field @@ -9,6 +15,19 @@ logger = logging.getLogger(__name__) @dataclass 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 musicapps: MusicApps = field(repr=False) is_root: bool @@ -16,6 +35,16 @@ class Category: lazy: bool 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.musicapps = musicapps or MusicApps(lazy=lazy) self.is_root = False @@ -26,6 +55,12 @@ class Category: self.load() 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}") if self.name not in self.musicapps.tagpool.categories.keys(): raise CategoryValidationError(f"Category {self.name} not found in tagpool") @@ -41,6 +76,21 @@ class Category: @classmethod 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}") if musicapps is None: musicapps = MusicApps() @@ -58,6 +108,11 @@ class Category: @property 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: return self return self.__class__( @@ -67,6 +122,14 @@ class 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__( f"{self.name}:{name}", musicapps=self.musicapps, lazy=self.lazy ) diff --git a/src/logic_plugin_manager/tags/musicapps.py b/src/logic_plugin_manager/tags/musicapps.py index b21259c..3e435f1 100644 --- a/src/logic_plugin_manager/tags/musicapps.py +++ b/src/logic_plugin_manager/tags/musicapps.py @@ -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 plistlib from dataclasses import dataclass, field @@ -10,6 +17,18 @@ logger = logging.getLogger(__name__) 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}") if not path.exists(): raise MusicAppsLoadError(f"File not found at {path}") @@ -23,6 +42,16 @@ def _parse_plist(path: Path): 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}") try: with open(path, "wb") as fp: @@ -34,9 +63,24 @@ def _save_plist(path: Path, data: dict): @dataclass 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] 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.lazy = lazy @@ -46,24 +90,60 @@ class Tagpool: self.load() 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}") self.categories = _parse_plist(self.path) logger.debug(f"Loaded Tagpool data from {self.path}") return self 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.categories[name] = plugin_count _save_plist(self.path, self.categories) self.load() 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() if name in self.categories: return self.write_category(name) 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.categories.pop(name, None) _save_plist(self.path, self.categories) @@ -72,11 +152,27 @@ class Tagpool: @dataclass 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] user_sorted: bool __raw_data: dict[str, str | list[str] | bool] = field(repr=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.lazy = lazy @@ -86,6 +182,14 @@ class Properties: self.load() 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}") self.__raw_data = _parse_plist(self.path) logger.debug(f"Loaded Properties data from {self.path}") @@ -122,6 +226,17 @@ class Properties: self.load() 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() sorting = self.sorting.copy() @@ -300,10 +415,26 @@ class Properties: @dataclass 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 properties: Properties 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.lazy = lazy @@ -313,16 +444,42 @@ class MusicApps: self.load() 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.properties = Properties(self.path, lazy=self.lazy) logger.debug(f"Loaded MusicApps from {self.path}") return self 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.properties.introduce_category(name) 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.properties.remove_category(name) diff --git a/src/logic_plugin_manager/tags/tagset.py b/src/logic_plugin_manager/tags/tagset.py index c4ed5c0..56f3aad 100644 --- a/src/logic_plugin_manager/tags/tagset.py +++ b/src/logic_plugin_manager/tags/tagset.py @@ -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 from dataclasses import dataclass, field from pathlib import Path @@ -11,6 +17,19 @@ from ..exceptions import ( @dataclass 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 nickname: str shortname: str @@ -18,6 +37,15 @@ class Tagset: __raw_data: dict[str, str | dict[str, str]] = field(repr=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.lazy = lazy @@ -25,6 +53,18 @@ class Tagset: self.load() 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(): raise NonexistentTagsetError(f".tagset not found at {self.path}") try: @@ -35,6 +75,13 @@ class Tagset: raise CannotParseTagsetError(f"An error occurred: {e}") from e 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: with open(self.path, "wb") as fp: plistlib.dump(self.__raw_data, fp) @@ -42,6 +89,15 @@ class Tagset: raise TagsetWriteError(f"An error occurred: {e}") from e 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.tags_id = self.path.name.removesuffix(".tagset") @@ -52,36 +108,99 @@ class Tagset: return self 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.__raw_data["nickname"] = nickname self._write_plist() self.load() 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.__raw_data["shortname"] = shortname self._write_plist() self.load() 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.__raw_data["tags"] = tags self._write_plist() self.load() 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.tags[tag] = value self._write_plist() self.load() 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() del self.tags[tag] self._write_plist() self.load() 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.tags.clear() self.tags[tag] = value diff --git a/uv.lock b/uv.lock index 47f9b09..786dd5d 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,166 @@ version = 1 revision = 3 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]] name = "logic-plugin-manager" -version = "0.1.0" +version = "0.0.1" source = { editable = "." } [package.optional-dependencies] @@ -14,8 +171,11 @@ search = [ [package.dev-dependencies] dev = [ + { name = "furo" }, { name = "rapidfuzz" }, { name = "rich" }, + { name = "sphinx" }, + { name = "sphinx-autodoc-typehints" }, ] [package.metadata] @@ -24,8 +184,11 @@ provides-extras = ["search"] [package.metadata.requires-dev] dev = [ + { name = "furo", specifier = ">=2025.9.25" }, { name = "rapidfuzz", specifier = ">=3.14.3" }, { name = "rich", specifier = ">=14.2.0" }, + { name = "sphinx", specifier = ">=8.2.3" }, + { name = "sphinx-autodoc-typehints", specifier = ">=3.5.2" }, ] [[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" }, ] +[[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]] name = "mdurl" 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" }, ] +[[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]] name = "pygments" 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" }, ] +[[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]] name = "rich" version = "14.2.0" @@ -122,3 +361,154 @@ sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7b 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" }, ] + +[[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" }, +]