From af8d17cea59cc4a1bef1488cbe4699c09ffc4ea0 Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:45:04 -0400 Subject: [PATCH 01/11] add package layer skeleton theory/, composition/, protocol/, api/ directories for architecture refactor --- midigen/api/__init__.py | 1 + midigen/composition/__init__.py | 1 + midigen/protocol/__init__.py | 1 + midigen/theory/__init__.py | 17 +++++++++++++++++ 4 files changed, 20 insertions(+) create mode 100644 midigen/api/__init__.py create mode 100644 midigen/composition/__init__.py create mode 100644 midigen/protocol/__init__.py create mode 100644 midigen/theory/__init__.py diff --git a/midigen/api/__init__.py b/midigen/api/__init__.py new file mode 100644 index 0000000..5a49135 --- /dev/null +++ b/midigen/api/__init__.py @@ -0,0 +1 @@ +"""High-level API layer — depends on all layers.""" diff --git a/midigen/composition/__init__.py b/midigen/composition/__init__.py new file mode 100644 index 0000000..672a684 --- /dev/null +++ b/midigen/composition/__init__.py @@ -0,0 +1 @@ +"""Composition layer — depends only on theory/.""" diff --git a/midigen/protocol/__init__.py b/midigen/protocol/__init__.py new file mode 100644 index 0000000..5689da0 --- /dev/null +++ b/midigen/protocol/__init__.py @@ -0,0 +1 @@ +"""MIDI protocol layer — depends on theory/, TYPE_CHECKING only for composition/.""" diff --git a/midigen/theory/__init__.py b/midigen/theory/__init__.py new file mode 100644 index 0000000..62d8675 --- /dev/null +++ b/midigen/theory/__init__.py @@ -0,0 +1,17 @@ +"""Music theory layer — pure logic, zero midigen dependencies.""" + +from midigen.theory.note import Note, NOTE_ON, NOTE_OFF +from midigen.theory.key import Key, KEY_MAP, VALID_KEYS +from midigen.theory.scale import Scale +from midigen.theory.time_utils import TimeConverter +from midigen.theory.roman import ( + ChordQuality, + ParsedRomanNumeral, + parse_roman_numeral, + get_root_pitch, + get_chord_pitches, + get_note_names_for_pitches, + CHORD_INTERVALS, + MAJOR_SCALE_SEMITONES, + MINOR_SCALE_SEMITONES, +) From e552ed060ab8b03b529129c8b1f2242450ab0470 Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:47:07 -0400 Subject: [PATCH 02/11] move theory layer to midigen/theory/ note, key, scale, roman, time_utils now live in midigen/theory/. All imports updated across source and tests. --- ...2026-04-12-architecture-refactor-design.md | 233 +++++++++ midigen/__init__.py | 22 +- midigen/chord.py | 6 +- midigen/compiler/midi_compiler.py | 4 +- midigen/drums.py | 2 +- midigen/melody.py | 6 +- midigen/midigen.py | 2 +- midigen/song.py | 2 +- midigen/tests/test_chord.py | 6 +- midigen/tests/test_drums.py | 2 +- midigen/tests/test_key.py | 2 +- midigen/tests/test_midigen.py | 4 +- midigen/tests/test_note.py | 4 +- midigen/tests/test_roman.py | 2 +- midigen/tests/test_scale.py | 4 +- midigen/tests/test_track.py | 4 +- midigen/{ => theory}/key.py | 0 midigen/{ => theory}/note.py | 0 midigen/{ => theory}/roman.py | 0 midigen/{ => theory}/scale.py | 0 midigen/{ => theory}/time_utils.py | 0 midigen/track.py | 4 +- poetry.lock | 469 ------------------ pyproject.toml | 29 +- uv.lock | 382 ++++++++++++++ 25 files changed, 669 insertions(+), 520 deletions(-) create mode 100644 docs/superpowers/specs/2026-04-12-architecture-refactor-design.md rename midigen/{ => theory}/key.py (100%) rename midigen/{ => theory}/note.py (100%) rename midigen/{ => theory}/roman.py (100%) rename midigen/{ => theory}/scale.py (100%) rename midigen/{ => theory}/time_utils.py (100%) delete mode 100644 poetry.lock create mode 100644 uv.lock diff --git a/docs/superpowers/specs/2026-04-12-architecture-refactor-design.md b/docs/superpowers/specs/2026-04-12-architecture-refactor-design.md new file mode 100644 index 0000000..dfb89dd --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-architecture-refactor-design.md @@ -0,0 +1,233 @@ +# Midigen Architecture Refactor + +## Goal + +Restructure midigen into explicit architectural layers with clean dependency boundaries, reduce implementation duplication via data-driven patterns, eliminate circular imports, and make the codebase trivially navigable — every file has real code, no shims, no ghosts. + +## Current State + +- 6,055 lines of source across 14 files + 1 subdirectory in a flat `midigen/` package +- `Chord` class: 44 methods (22 are copy-paste factory methods with identical structure) +- Circular import between `Song` ↔ `MidiCompiler` (late import hack at EOF) +- `MidiCompiler`: 9 direct dependencies (orchestration hub) +- Inconsistent factory patterns (free functions vs classmethods vs static methods) +- `Section`: no validation, fails deep in compilation with confusing errors +- No explicit layer boundaries — all modules at same package level +- `Track`: 22 methods — fine, but imports composition types it only needs for type hints +- `song_example.py` uses deprecated API (`song.generate()`) +- Tests ship in PyPI wheel (pre-existing, note for later) + +## Target Architecture + +``` +midigen/ +├── theory/ # Zero internal deps. Pure music logic. +│ ├── __init__.py # Exports: Note, Key, KEY_MAP, VALID_KEYS, Scale, TimeConverter, roman helpers +│ ├── note.py # Note class +│ ├── key.py # Key class + KEY_MAP + VALID_KEYS +│ ├── scale.py # Scale class (@staticmethod — stays unchanged) +│ ├── roman.py # Roman numeral parsing (parse_roman_numeral, get_chord_pitches, etc.) +│ └── time_utils.py # TimeConverter (pure math, no external imports) +│ +├── composition/ # Depends ONLY on theory/ +│ ├── __init__.py # Exports: Chord, CHORD_TYPES, ChordProgression, Arpeggio, ArpeggioPattern, Melody, DrumKit, Drum, GM1_DRUM_MAP, Section +│ ├── chord.py # Chord class (container + voicings + data-driven factories) +│ ├── progression.py # ChordProgression (roman numeral → chord sequence) +│ ├── arpeggio.py # Arpeggio + ArpeggioPattern +│ ├── melody.py # Melody generation +│ ├── drums.py # DrumKit, Drum, GM1_DRUM_MAP +│ └── section.py # Section with validation +│ +├── protocol/ # Depends on theory/ at runtime, TYPE_CHECKING only for composition/ +│ ├── __init__.py # Exports: Track, MAX_MIDI_TICKS, ChannelPool, ChannelExhaustedError, INSTRUMENT_MAP +│ ├── track.py # Track (note collection + MIDI compilation, single class) +│ ├── channel_pool.py # ChannelPool, ChannelExhaustedError +│ └── instruments.py # INSTRUMENT_MAP +│ +├── api/ # Depends on all layers +│ ├── __init__.py # Exports: Song, MidiGen, MidiCompiler +│ ├── song.py # Song (pure data container) +│ ├── midigen.py # MidiGen (low-level MidiFile wrapper) +│ └── compiler.py # MidiCompiler (orchestration) +│ +├── tests/ # Test suite (stays in place) +│ ├── __init__.py +│ └── test_*.py # All imports updated to new paths +│ +└── __init__.py # Public API re-exports (the ONLY backwards-compat layer) +``` + +**No shim files.** The old `midigen/chord.py`, `midigen/track.py`, etc. are GONE — moved to their new locations via `git mv`. The only backwards-compat mechanism is the top-level `__init__.py` which re-exports all public names. + +## Dependency Rules + +1. `theory/` imports NOTHING from midigen +2. `composition/` imports ONLY from `theory/` +3. `protocol/` imports from `theory/` at runtime. Uses `from __future__ import annotations` + `TYPE_CHECKING` for composition/ type annotations only. +4. `api/` imports from all three layers freely +5. NO circular imports — compiler.py imports Song at top level (no late-import hack) +6. Top-level `__init__.py` re-exports the full public API + +## Key Design Decisions + +### Decision 1: No Shim Files + +Shim files (re-export stubs at old locations) create: +- 14 extra files cluttering the package root +- Developer confusion (opening `midigen/chord.py` and finding a 3-line redirect) +- A maintenance trap (3 places to update for each new export) +- The illusion of backwards compat while actually making navigation worse + +Instead: the top-level `__init__.py` is the single public API surface. `from midigen import Chord` works. `from midigen.chord import Chord` does NOT work (old path deleted). All internal imports (tests, source, examples) are updated to canonical paths. + +This means: any external user doing `from midigen.chord import Chord` needs to change to `from midigen import Chord`. One-line fix. Justified at version 1.0.0. + +### Decision 2: Don't Split Track + +Track has 22 methods serving one concept. The `compile()` method is 40 lines. Not worth splitting. + +**What we fix:** Add `from __future__ import annotations` and move Chord/ChordProgression/Arpeggio/DrumKit behind `if TYPE_CHECKING:`. Track's convenience methods duck-type (call `.get_chord()`, `.get_progression()`, etc.) — they don't need these types at runtime. + +Note: `Key` stays as a runtime import because `set_key_signature()` does `isinstance(key, Key)`. + +### Decision 3: Data-Driven Chord Factories + +Add a `CHORD_TYPES` dict mapping string names to interval tuples: +```python +CHORD_TYPES = { + "major_triad": (0, 4, 7), + "minor_triad": (0, 3, 7), + "sus2": (0, 2, 7), + "sus4": (0, 5, 7), + "augmented": (0, 4, 8), + "diminished": (0, 3, 6), + "dominant_seventh": (0, 4, 7, 10), + "major_seventh": (0, 4, 7, 11), + "minor_seventh": (0, 3, 7, 10), + # ... all 22 types +} +``` + +Add `Chord.build(chord_type: str, root: Note) -> Chord` classmethod. +Keep ALL 22 `create_*` classmethods as one-line wrappers: `return cls.build("sus2", root)`. + +**Why keep the wrappers:** Zero API breakage. External code calling `Chord.create_sus2(root)` still works. Tests don't change. But internally, adding a new chord type is now one dict entry. + +**Why add build():** Programmatic access. Users can list available types via `CHORD_TYPES.keys()`, build chords dynamically, and register custom types in the future. + +### Decision 4: Extract ChordProgression and Arpeggio + +These are genuinely separate concerns from Chord: +- ChordProgression: parses roman numerals, manages chord sequences, handles timing +- Arpeggio: extends Chord with sequential playback patterns and looping + +Extracting them gives each its own file with a single clear purpose. + +### Decision 5: Song Decoupling + +Remove from Song: `_get_compiler()`, `_compiler`, `generate()`, `save()`, `midigen` property, `available_channels` property. Song becomes a 40-line data container. + +MidiCompiler imports Song at the top of the file (no circular dependency since Song doesn't reference MidiCompiler). The late-import hack at EOF of midi_compiler.py is deleted. + +### Decision 6: Section Validation + +Section.__init__ validates the chord progression string: +- Non-empty +- Split on '-', each token passed to `parse_roman_numeral()` +- Raises ValueError with clear message on first invalid token + +### Decision 7: Feature Branch + +All work on `refactor/architecture-layers`. Merge via PR after full verification. + +### Decision 8: Scale Stays Unchanged + +Scale uses `@staticmethod`. It's never instantiated, never subclassed. No reason to change to `@classmethod`. Don't standardize for standardization's sake. + +## Import Updates Required + +53 submodule imports across 19 files need updating. Examples: + +| Old Path | New Path | +|---|---| +| `from midigen.note import Note` | `from midigen.theory.note import Note` | +| `from midigen.chord import Chord, ChordProgression` | `from midigen.composition.chord import Chord` + `from midigen.composition.progression import ChordProgression` | +| `from midigen.track import Track, MAX_MIDI_TICKS` | `from midigen.protocol.track import Track, MAX_MIDI_TICKS` | +| `from midigen.midigen import MidiGen` | `from midigen.api.midigen import MidiGen` | +| `from midigen.compiler import MidiCompiler` | `from midigen.api.compiler import MidiCompiler` | +| `from midigen.drums import DrumKit, Drum, GM1_DRUM_MAP` | `from midigen.composition.drums import DrumKit, Drum, GM1_DRUM_MAP` | +| `from midigen.key import Key, KEY_MAP, VALID_KEYS` | `from midigen.theory.key import Key, KEY_MAP, VALID_KEYS` | +| `from midigen.channel_pool import ChannelPool` | `from midigen.protocol.channel_pool import ChannelPool` | +| `from midigen.instruments import INSTRUMENT_MAP` | `from midigen.protocol.instruments import INSTRUMENT_MAP` | +| `from midigen.time_utils import TimeConverter` | `from midigen.theory.time_utils import TimeConverter` | +| `from midigen.roman import parse_roman_numeral, ...` | `from midigen.theory.roman import parse_roman_numeral, ...` | +| `from midigen.scale import Scale` | `from midigen.theory.scale import Scale` | + +Tests that use `from midigen import X` (via __init__.py) don't need changes. + +## Tests That Change + +**Deleted tests** (test deprecated API that's being removed): +- `test_song.py::TestLegacyAPI` (3 tests: test_legacy_generate, test_legacy_available_channels, test_deprecation_warning_on_generate) + +**Modified tests:** +- `test_golden_master.py::test_song_progression_i_v_vi_iv`: rewrite from `song.generate()` → MidiCompiler, verify MIDI output is byte-identical +- `test_integration.py`: remove 5 calls to deprecated `track.add_note_off_messages()` +- All test files: update submodule import paths (mechanical find-replace) + +**New tests:** +- Section validation: empty string, malformed tokens, double dashes, trailing dashes +- `Chord.build()`: verify all CHORD_TYPES produce correct intervals, invalid type name raises KeyError + +## Non-Test Files That Change + +- `example.py`: update imports to `from midigen import MidiGen, Note, Chord, Key, KEY_MAP` +- `song_example.py`: rewrite to use MidiCompiler instead of deprecated song.generate()/save() +- `README.md`: update all code examples to current API + +## Execution Order + +0. Create feature branch `refactor/architecture-layers` +1. Create package skeleton (theory/, composition/, protocol/, api/ with __init__.py) +2. Move theory layer (git mv note, key, scale, roman, time_utils → theory/) + update internal imports + update __init__.py → tests WILL break (imports wrong), fix test imports → tests pass +3. Refactor Chord: add CHORD_TYPES dict + build() method + convert create_* to wrappers. Extract ChordProgression → composition/progression.py. Extract Arpeggio → composition/arpeggio.py. Move chord.py → composition/. Update imports everywhere. → tests pass +4. Move melody, drums, section → composition/. Add Section validation. Update imports. Add new Section validation tests. → tests pass +5. Move track, channel_pool, instruments → protocol/. Add `from __future__ import annotations` + TYPE_CHECKING to track.py. Update imports. → tests pass +6. Decouple Song + move song, midigen, compiler → api/. Remove deprecated Song methods. Delete TestLegacyAPI tests. Rewrite golden master test. Update imports. → tests pass +7. Remove Track.add_note_off_messages() + remove calls in test_integration.py → tests pass, zero DeprecationWarnings +8. Update example.py, song_example.py, README.md +9. Final verification (layer isolation, golden masters, all public imports) +10. PR to main + +## Success Criteria + +1. All tests pass (some deleted, some modified, some added) +2. No circular imports (import each subpackage in isolation) +3. Layer isolation verified: `python -c "from midigen.theory.note import Note"` doesn't pull in composition/protocol/api +4. `Chord.build()` exists and CHORD_TYPES is a public dict +5. `Section("x", 1, "garbage")` raises ValueError +6. `grep -r "MidiCompiler\|_compiler\|generate\|\.save" midigen/api/song.py` returns nothing +7. Golden master MIDI output is byte-identical after test rewrite +8. `from midigen import Chord, Song, MidiCompiler, Track, Note, Key, ...` works (full public API via __init__) +9. Zero DeprecationWarnings in pytest output +10. example.py and song_example.py use current API and run successfully +11. README shows current, non-deprecated usage + +## What This Plan Does NOT Do + +- Split Track (compile() is 40 lines, not a class) +- Reorganize test directory (cosmetic, risky) +- Change Scale's @staticmethod pattern (no reason) +- Delete any public API method (all create_* stay as wrappers) +- Add Protocol types or builder patterns (future) +- Add comprehensive error messages (future) +- Fix tests-in-wheel issue (note for separate PR) +- Create shim/redirect files (they're worse than the problem they solve) + +## Known Risks + +1. **`from midigen.chord import Chord` breaks for external users.** Mitigated: top-level `from midigen import Chord` is the documented API. Anyone using submodule paths fixes with one-line change. This is 1.0.0 — submodule layout is an implementation detail. +2. **Step 6 is highest risk**: removing Song deprecated methods + rewriting golden master test. Must verify MIDI binary is identical before/after. +3. **`from __future__ import annotations` in track.py**: Makes all annotations strings. Fine because Track never does isinstance checks against Chord/ChordProgression — only against Note and Key (both runtime-imported from theory/). +4. **Section validation could reject currently-working input**: If any existing test or example passes a string that `parse_roman_numeral()` can't handle, the validation will break it. Must verify all existing Section usages parse cleanly. +5. **CHORD_TYPES interval overlap with roman.py CHORD_INTERVALS**: Different dicts for different purposes. roman.py maps ChordQuality enums → intervals (for parsing). CHORD_TYPES maps string names → intervals (for construction). Not worth unifying — different keys, different consumers. diff --git a/midigen/__init__.py b/midigen/__init__.py index ec108ee..fe7a9be 100644 --- a/midigen/__init__.py +++ b/midigen/__init__.py @@ -1,14 +1,18 @@ -from .midigen import MidiGen -from .track import Track -from .note import Note +from .theory.note import Note +from .theory.key import Key, KEY_MAP +from .theory.scale import Scale +from .theory.time_utils import TimeConverter +from .theory.roman import parse_roman_numeral, get_chord_pitches + from .chord import Chord, ChordProgression, Arpeggio, ArpeggioPattern -from .key import Key, KEY_MAP -from .scale import Scale from .drums import DrumKit, Drum -from .song import Song -from .section import Section -from .instruments import INSTRUMENT_MAP -from .time_utils import TimeConverter from .melody import Melody +from .section import Section + +from .track import Track from .channel_pool import ChannelPool, ChannelExhaustedError +from .instruments import INSTRUMENT_MAP + +from .midigen import MidiGen +from .song import Song from .compiler import MidiCompiler diff --git a/midigen/chord.py b/midigen/chord.py index cb289b8..46a9a7e 100644 --- a/midigen/chord.py +++ b/midigen/chord.py @@ -1,7 +1,7 @@ from typing import List -from midigen.note import Note -from midigen.key import KEY_MAP, Key -from midigen.roman import get_chord_pitches +from midigen.theory.note import Note +from midigen.theory.key import KEY_MAP, Key +from midigen.theory.roman import get_chord_pitches from enum import Enum diff --git a/midigen/compiler/midi_compiler.py b/midigen/compiler/midi_compiler.py index dc66c3a..60e8b30 100644 --- a/midigen/compiler/midi_compiler.py +++ b/midigen/compiler/midi_compiler.py @@ -36,8 +36,8 @@ from typing import Dict, List, Optional from midigen.midigen import MidiGen -from midigen.key import Key -from midigen.note import Note +from midigen.theory.key import Key +from midigen.theory.note import Note from midigen.chord import Chord, ChordProgression from midigen.channel_pool import ChannelPool, ChannelExhaustedError from midigen.instruments import INSTRUMENT_MAP diff --git a/midigen/drums.py b/midigen/drums.py index ab987c3..d56b274 100644 --- a/midigen/drums.py +++ b/midigen/drums.py @@ -1,4 +1,4 @@ -from midigen.note import Note +from midigen.theory.note import Note from typing import List GM1_DRUM_MAP = { diff --git a/midigen/melody.py b/midigen/melody.py index 82ba8b8..5515151 100644 --- a/midigen/melody.py +++ b/midigen/melody.py @@ -3,9 +3,9 @@ """ from typing import List, Union -from midigen.note import Note -from midigen.key import KEY_MAP -from midigen.time_utils import TimeConverter +from midigen.theory.note import Note +from midigen.theory.key import KEY_MAP +from midigen.theory.time_utils import TimeConverter import random diff --git a/midigen/midigen.py b/midigen/midigen.py index 3afd5de..97a4f94 100644 --- a/midigen/midigen.py +++ b/midigen/midigen.py @@ -1,7 +1,7 @@ from typing import Tuple import os from mido import MidiFile -from midigen.key import Key +from midigen.theory.key import Key from midigen.track import Track from pathlib import Path diff --git a/midigen/song.py b/midigen/song.py index 7af020f..5cd4c9a 100644 --- a/midigen/song.py +++ b/midigen/song.py @@ -27,7 +27,7 @@ import warnings from typing import List, Set -from midigen.key import Key +from midigen.theory.key import Key from midigen.section import Section diff --git a/midigen/tests/test_chord.py b/midigen/tests/test_chord.py index 9712351..7c7b89d 100644 --- a/midigen/tests/test_chord.py +++ b/midigen/tests/test_chord.py @@ -1,5 +1,5 @@ -from midigen.note import Note -from midigen.key import KEY_MAP +from midigen.theory.note import Note +from midigen.theory.key import KEY_MAP from midigen.chord import Chord, ChordProgression, Arpeggio, ArpeggioPattern import unittest @@ -254,7 +254,7 @@ def test_arpeggio_multiple_loops(self): self.assertEqual(sequential_notes[6].time, 300) # Loop 3 -from midigen.key import Key +from midigen.theory.key import Key class TestChordProgressionFromRomanNumerals(unittest.TestCase): diff --git a/midigen/tests/test_drums.py b/midigen/tests/test_drums.py index 256aefd..c8c9931 100644 --- a/midigen/tests/test_drums.py +++ b/midigen/tests/test_drums.py @@ -1,6 +1,6 @@ import unittest from midigen.drums import DrumKit, Drum, GM1_DRUM_MAP -from midigen.note import Note +from midigen.theory.note import Note class TestDrumKit(unittest.TestCase): diff --git a/midigen/tests/test_key.py b/midigen/tests/test_key.py index 41ad95a..9aa87ad 100644 --- a/midigen/tests/test_key.py +++ b/midigen/tests/test_key.py @@ -1,4 +1,4 @@ -from midigen.key import Key, VALID_KEYS +from midigen.theory.key import Key, VALID_KEYS import unittest class TestKey(unittest.TestCase): diff --git a/midigen/tests/test_midigen.py b/midigen/tests/test_midigen.py index 82d9cf6..d015902 100644 --- a/midigen/tests/test_midigen.py +++ b/midigen/tests/test_midigen.py @@ -1,8 +1,8 @@ import os from midigen.midigen import MidiGen -from midigen.note import Note +from midigen.theory.note import Note from mido import MidiFile -from midigen.key import Key +from midigen.theory.key import Key import unittest from mido import bpm2tempo, Message diff --git a/midigen/tests/test_note.py b/midigen/tests/test_note.py index eaa827c..3232cde 100644 --- a/midigen/tests/test_note.py +++ b/midigen/tests/test_note.py @@ -1,5 +1,5 @@ -from midigen.note import Note -from midigen.key import KEY_MAP +from midigen.theory.note import Note +from midigen.theory.key import KEY_MAP import unittest diff --git a/midigen/tests/test_roman.py b/midigen/tests/test_roman.py index a0a390e..7b7a639 100644 --- a/midigen/tests/test_roman.py +++ b/midigen/tests/test_roman.py @@ -7,7 +7,7 @@ """ import unittest -from midigen.roman import ( +from midigen.theory.roman import ( parse_roman_numeral, get_root_pitch, get_chord_pitches, diff --git a/midigen/tests/test_scale.py b/midigen/tests/test_scale.py index 0bb3678..3d1a354 100644 --- a/midigen/tests/test_scale.py +++ b/midigen/tests/test_scale.py @@ -1,5 +1,5 @@ -from midigen.scale import Scale -from midigen.key import KEY_MAP +from midigen.theory.scale import Scale +from midigen.theory.key import KEY_MAP import unittest diff --git a/midigen/tests/test_track.py b/midigen/tests/test_track.py index 4592446..4617644 100644 --- a/midigen/tests/test_track.py +++ b/midigen/tests/test_track.py @@ -1,6 +1,6 @@ from midigen.midigen import MidiGen -from midigen.note import Note -from midigen.key import KEY_MAP +from midigen.theory.note import Note +from midigen.theory.key import KEY_MAP from midigen.chord import Chord, Arpeggio from midigen.track import MAX_MIDI_TICKS import unittest diff --git a/midigen/key.py b/midigen/theory/key.py similarity index 100% rename from midigen/key.py rename to midigen/theory/key.py diff --git a/midigen/note.py b/midigen/theory/note.py similarity index 100% rename from midigen/note.py rename to midigen/theory/note.py diff --git a/midigen/roman.py b/midigen/theory/roman.py similarity index 100% rename from midigen/roman.py rename to midigen/theory/roman.py diff --git a/midigen/scale.py b/midigen/theory/scale.py similarity index 100% rename from midigen/scale.py rename to midigen/theory/scale.py diff --git a/midigen/time_utils.py b/midigen/theory/time_utils.py similarity index 100% rename from midigen/time_utils.py rename to midigen/theory/time_utils.py diff --git a/midigen/track.py b/midigen/track.py index a9afa26..cf887a6 100644 --- a/midigen/track.py +++ b/midigen/track.py @@ -4,8 +4,8 @@ from midigen.chord import Chord, ChordProgression, Arpeggio -from midigen.key import Key -from midigen.note import Note +from midigen.theory.key import Key +from midigen.theory.note import Note from midigen.drums import DrumKit diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 13ca654..0000000 --- a/poetry.lock +++ /dev/null @@ -1,469 +0,0 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "distlib" -version = "0.3.7" -description = "Distribution utilities" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.20.3" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, - {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, -] - -[[package]] -name = "flake8" -version = "7.3.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, - {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.14.0,<2.15.0" -pyflakes = ">=3.4.0,<3.5.0" - -[[package]] -name = "identify" -version = "2.5.26" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"}, - {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mido" -version = "1.3.3" -description = "MIDI Objects for Python" -optional = false -python-versions = "~=3.7" -groups = ["main"] -files = [ - {file = "mido-1.3.3-py3-none-any.whl", hash = "sha256:01033c9b10b049e4436fca2762194ca839b09a4334091dd3c34e7f4ae674fd8a"}, - {file = "mido-1.3.3.tar.gz", hash = "sha256:1aecb30b7f282404f17e43768cbf74a6a31bf22b3b783bdd117a1ce9d22cb74c"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -build-docs = ["sphinx (>=4.3.2,<4.4.0)", "sphinx-rtd-theme (>=1.2.2,<1.3.0)"] -check-manifest = ["check-manifest (>=0.49)"] -dev = ["mido[build-docs]", "mido[check-manifest]", "mido[lint-code]", "mido[lint-reuse]", "mido[release]", "mido[test-code]"] -lint-code = ["ruff (>=0.1.6,<0.2.0)"] -lint-reuse = ["reuse (>=1.1.2,<1.2.0)"] -ports-all = ["mido[ports-pygame]", "mido[ports-rtmidi-python]", "mido[ports-rtmidi]"] -ports-pygame = ["PyGame (>=2.5,<3.0)"] -ports-rtmidi = ["python-rtmidi (>=1.5.4,<1.6.0)"] -ports-rtmidi-python = ["rtmidi-python (>=0.2.2,<0.3.0)"] -release = ["twine (>=4.0.2,<4.1.0)"] -test-code = ["pytest (>=7.4.0,<7.5.0)"] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -groups = ["dev"] -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "23.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, -] - -[[package]] -name = "platformdirs" -version = "3.10.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, -] - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "4.5.1" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77"}, - {file = "pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pycodestyle" -version = "2.14.0" -description = "Python style guide checker" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, - {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, -] - -[[package]] -name = "pyflakes" -version = "3.4.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, - {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, -] - -[[package]] -name = "pygments" -version = "2.20.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pytest" -version = "9.0.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, - {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "setuptools" -version = "78.1.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"}, - {file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "tomli" -version = "2.3.0" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "virtualenv" -version = "20.36.1" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, - {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""} -platformdirs = ">=3.9.1,<5" -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] - -[metadata] -lock-version = "2.1" -python-versions = "^3.10" -content-hash = "3d9934dbb65b4a7deefc2821e00d173f55f41d7d27df8a9061ac10649be09bf6" diff --git a/pyproject.toml b/pyproject.toml index eeb95b8..5546174 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,23 @@ -[tool.poetry] -name = "midigen-lib" # distribution name on PyPI -packages = [{ include = "midigen" }] +[project] +name = "midigen-lib" version = "1.0.0" description = "High-level, theory-powered MIDI generation library" -authors = ["cainky "] +authors = [{ name = "cainky", email = "kylecain.me@gmail.com" }] readme = "README.md" license = "GPL-3.0-only" -repository = "https://github.com/cainky/midigen" +requires-python = ">=3.10" keywords = ["midi", "music-generation", "mido", "music-theory"] +dependencies = ["mido>=1.3.2,<2"] -[tool.poetry.dependencies] -python = "^3.10" -mido = "^1.3.2" +[project.urls] +Repository = "https://github.com/cainky/midigen" - -[tool.poetry.group.dev.dependencies] -pre-commit = ">=3.6,<5.0" -flake8 = ">=6.1,<8.0" -pytest = "^9.0.1" +[dependency-groups] +dev = ["pre-commit>=3.6,<5.0", "flake8>=6.1,<8.0", "pytest>=9.0.1,<10"] [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["midigen"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8cd73cf --- /dev/null +++ b/uv.lock @@ -0,0 +1,382 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[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 = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "identify" +version = "2.6.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "midigen-lib" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "mido" }, +] + +[package.dev-dependencies] +dev = [ + { name = "flake8" }, + { name = "pre-commit" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "mido", specifier = ">=1.3.2,<2" }] + +[package.metadata.requires-dev] +dev = [ + { name = "flake8", specifier = ">=6.1,<8.0" }, + { name = "pre-commit", specifier = ">=3.6,<5.0" }, + { name = "pytest", specifier = ">=9.0.1,<10" }, +] + +[[package]] +name = "mido" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/14/cfda3fe61ce4c0f50a9f707ae02b46cb53211732b2cd4522bf06272848f4/mido-1.3.3.tar.gz", hash = "sha256:1aecb30b7f282404f17e43768cbf74a6a31bf22b3b783bdd117a1ce9d22cb74c", size = 124288, upload-time = "2024-10-25T15:05:21.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/28/45deb15c11859d2f10702b32e71de9328a9fa494f989626916db39a9617f/mido-1.3.3-py3-none-any.whl", hash = "sha256:01033c9b10b049e4436fca2762194ca839b09a4334091dd3c34e7f4ae674fd8a", size = 54614, upload-time = "2024-10-25T15:05:20.349Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[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 = "virtualenv" +version = "21.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/c5/aff062c66b42e2183201a7ace10c6b2e959a9a16525c8e8ca8e59410d27a/virtualenv-21.2.1.tar.gz", hash = "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", size = 5844770, upload-time = "2026-04-09T18:47:11.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/0e/f083a76cb590e60dff3868779558eefefb8dfb7c9ed020babc7aa014ccbf/virtualenv-21.2.1-py3-none-any.whl", hash = "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2", size = 5828326, upload-time = "2026-04-09T18:47:09.331Z" }, +] From 44038a84fa2374bfb587ea75378bc4817735d450 Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:49:07 -0400 Subject: [PATCH 03/11] refactor chord with data-driven factories, extract progression and arpeggio - CHORD_TYPES dict + Chord.build() replaces duplicated interval logic - All create_* classmethods preserved as one-liner wrappers - ChordProgression extracted to composition/progression.py - Arpeggio + ArpeggioPattern extracted to composition/arpeggio.py - Zero API breakage, zero test logic changes --- midigen/__init__.py | 4 +- midigen/chord.py | 390 ----------------------------- midigen/compiler/midi_compiler.py | 3 +- midigen/composition/__init__.py | 4 + midigen/composition/arpeggio.py | 40 +++ midigen/composition/chord.py | 260 +++++++++++++++++++ midigen/composition/progression.py | 77 ++++++ midigen/tests/test_chord.py | 4 +- midigen/tests/test_track.py | 3 +- midigen/track.py | 4 +- 10 files changed, 394 insertions(+), 395 deletions(-) delete mode 100644 midigen/chord.py create mode 100644 midigen/composition/arpeggio.py create mode 100644 midigen/composition/chord.py create mode 100644 midigen/composition/progression.py diff --git a/midigen/__init__.py b/midigen/__init__.py index fe7a9be..eb4e0de 100644 --- a/midigen/__init__.py +++ b/midigen/__init__.py @@ -4,7 +4,9 @@ from .theory.time_utils import TimeConverter from .theory.roman import parse_roman_numeral, get_chord_pitches -from .chord import Chord, ChordProgression, Arpeggio, ArpeggioPattern +from .composition.chord import Chord, CHORD_TYPES +from .composition.progression import ChordProgression +from .composition.arpeggio import Arpeggio, ArpeggioPattern from .drums import DrumKit, Drum from .melody import Melody from .section import Section diff --git a/midigen/chord.py b/midigen/chord.py deleted file mode 100644 index 46a9a7e..0000000 --- a/midigen/chord.py +++ /dev/null @@ -1,390 +0,0 @@ -from typing import List -from midigen.theory.note import Note -from midigen.theory.key import KEY_MAP, Key -from midigen.theory.roman import get_chord_pitches -from enum import Enum - - -class Chord: - def __init__(self, notes: List[Note]): - self.notes = notes - self.root = self.get_root() - self._calculate_start_time() - self._calculate_duration() - - def __str__(self) -> str: - return f"[{', '.join(str(note) for note in self.notes)}]" - - def _calculate_start_time(self) -> int: - """ - Calculate the start time of the chord. - - Returns: - The start time of the chord. - """ - self.time = min(note.time for note in self.notes) if self.notes else 0 - return self.time - - def _calculate_duration(self) -> int: - """ - Calculate the duration of the chord. - - Returns: - The duration of the longest note in the chord. - """ - if not self.notes: - return 0 - - # Find the earliest start time of any note in the chord - earliest_start_time = min(note.time for note in self.notes) - # Find the latest ending time, calculated as start time plus duration for each note - latest_end_time = max(note.time + note.duration for note in self.notes) - - # The duration of the chord is the difference between the earliest start and the latest end - self.duration = latest_end_time - earliest_start_time - return self.duration - - def add_note(self, note: Note) -> None: - self.notes.append(note) - self._calculate_duration() - self._calculate_start_time() - - def get_chord(self) -> List[Note]: - return self.notes - - def get_root(self) -> Note: - if self.notes: - self.root = self.notes[0] - return self.root - return None - - def major_triad(self) -> List[Note]: - return [self.root, self.root + 4, self.root + 7] - - def minor_triad(self) -> List[Note]: - return [self.root, self.root + 3, self.root + 7] - - def dominant_seventh(self) -> List[Note]: - return self.major_triad() + [self.root + 10] - - def major_seventh(self) -> List[Note]: - return self.major_triad() + [self.root + 11] - - def minor_seventh(self) -> List[Note]: - return self.minor_triad() + [self.root + 10] - - def half_diminished_seventh(self) -> List[Note]: - return [self.root, self.root + 3, self.root + 6, self.root + 10] - - def diminished_seventh(self) -> List[Note]: - return [self.root, self.root + 3, self.root + 6, self.root + 9] - - def minor_ninth(self) -> List[Note]: - return self.minor_seventh() + [self.root + 14] - - def major_ninth(self) -> List[Note]: - return self.major_seventh() + [self.root + 14] - - def dominant_ninth(self) -> List[Note]: - return self.dominant_seventh() + [self.root + 14] - - @classmethod - def create_major_triad(cls, root: Note) -> 'Chord': - """Create a major triad chord from a root note.""" - notes = [root, root + 4, root + 7] - return cls(notes) - - @classmethod - def create_minor_triad(cls, root: Note) -> 'Chord': - """Create a minor triad chord from a root note.""" - notes = [root, root + 3, root + 7] - return cls(notes) - - @classmethod - def create_dominant_seventh(cls, root: Note) -> 'Chord': - """Create a dominant seventh chord from a root note.""" - notes = [root, root + 4, root + 7, root + 10] - return cls(notes) - - @classmethod - def create_major_seventh(cls, root: Note) -> 'Chord': - """Create a major seventh chord from a root note.""" - notes = [root, root + 4, root + 7, root + 11] - return cls(notes) - - @classmethod - def create_minor_seventh(cls, root: Note) -> 'Chord': - """Create a minor seventh chord from a root note.""" - notes = [root, root + 3, root + 7, root + 10] - return cls(notes) - - @classmethod - def create_half_diminished_seventh(cls, root: Note) -> 'Chord': - """Create a half-diminished seventh chord from a root note.""" - notes = [root, root + 3, root + 6, root + 10] - return cls(notes) - - @classmethod - def create_diminished_seventh(cls, root: Note) -> 'Chord': - """Create a diminished seventh chord from a root note.""" - notes = [root, root + 3, root + 6, root + 9] - return cls(notes) - - @classmethod - def create_minor_ninth(cls, root: Note) -> 'Chord': - """Create a minor ninth chord from a root note.""" - notes = [root, root + 3, root + 7, root + 10, root + 14] - return cls(notes) - - @classmethod - def create_major_ninth(cls, root: Note) -> 'Chord': - """Create a major ninth chord from a root note.""" - notes = [root, root + 4, root + 7, root + 11, root + 14] - return cls(notes) - - @classmethod - def create_dominant_ninth(cls, root: Note) -> 'Chord': - """Create a dominant ninth chord from a root note.""" - notes = [root, root + 4, root + 7, root + 10, root + 14] - return cls(notes) - - # ===== Suspended Chords ===== - - @classmethod - def create_sus2(cls, root: Note) -> 'Chord': - """Create a suspended 2nd chord from a root note (root, 2nd, 5th).""" - notes = [root, root + 2, root + 7] - return cls(notes) - - @classmethod - def create_sus4(cls, root: Note) -> 'Chord': - """Create a suspended 4th chord from a root note (root, 4th, 5th).""" - notes = [root, root + 5, root + 7] - return cls(notes) - - # ===== Augmented and Diminished Triads ===== - - @classmethod - def create_augmented(cls, root: Note) -> 'Chord': - """Create an augmented triad from a root note (root, major 3rd, augmented 5th).""" - notes = [root, root + 4, root + 8] - return cls(notes) - - @classmethod - def create_diminished(cls, root: Note) -> 'Chord': - """Create a diminished triad from a root note (root, minor 3rd, diminished 5th).""" - notes = [root, root + 3, root + 6] - return cls(notes) - - # ===== 6th Chords ===== - - @classmethod - def create_major_sixth(cls, root: Note) -> 'Chord': - """Create a major 6th chord from a root note (major triad + 6th).""" - notes = [root, root + 4, root + 7, root + 9] - return cls(notes) - - @classmethod - def create_minor_sixth(cls, root: Note) -> 'Chord': - """Create a minor 6th chord from a root note (minor triad + 6th).""" - notes = [root, root + 3, root + 7, root + 9] - return cls(notes) - - # ===== 11th Chords ===== - - @classmethod - def create_dominant_eleventh(cls, root: Note) -> 'Chord': - """Create a dominant 11th chord from a root note.""" - notes = [root, root + 4, root + 7, root + 10, root + 14, root + 17] - return cls(notes) - - @classmethod - def create_major_eleventh(cls, root: Note) -> 'Chord': - """Create a major 11th chord from a root note.""" - notes = [root, root + 4, root + 7, root + 11, root + 14, root + 17] - return cls(notes) - - @classmethod - def create_minor_eleventh(cls, root: Note) -> 'Chord': - """Create a minor 11th chord from a root note.""" - notes = [root, root + 3, root + 7, root + 10, root + 14, root + 17] - return cls(notes) - - # ===== 13th Chords ===== - - @classmethod - def create_dominant_thirteenth(cls, root: Note) -> 'Chord': - """Create a dominant 13th chord from a root note.""" - notes = [root, root + 4, root + 7, root + 10, root + 14, root + 17, root + 21] - return cls(notes) - - @classmethod - def create_major_thirteenth(cls, root: Note) -> 'Chord': - """Create a major 13th chord from a root note.""" - notes = [root, root + 4, root + 7, root + 11, root + 14, root + 17, root + 21] - return cls(notes) - - @classmethod - def create_minor_thirteenth(cls, root: Note) -> 'Chord': - """Create a minor 13th chord from a root note.""" - notes = [root, root + 3, root + 7, root + 10, root + 14, root + 17, root + 21] - return cls(notes) - - # ===== Add Chords ===== - - @classmethod - def create_add9(cls, root: Note) -> 'Chord': - """Create an add9 chord from a root note (major triad + 9th, no 7th).""" - notes = [root, root + 4, root + 7, root + 14] - return cls(notes) - - @classmethod - def create_minor_add9(cls, root: Note) -> 'Chord': - """Create a minor add9 chord from a root note (minor triad + 9th, no 7th).""" - notes = [root, root + 3, root + 7, root + 14] - return cls(notes) - - @classmethod - def create_add11(cls, root: Note) -> 'Chord': - """Create an add11 chord from a root note (major triad + 11th, no 7th or 9th).""" - notes = [root, root + 4, root + 7, root + 17] - return cls(notes) - - # ===== Augmented 7th Chords ===== - - @classmethod - def create_augmented_seventh(cls, root: Note) -> 'Chord': - """Create an augmented 7th chord from a root note (augmented triad + minor 7th).""" - notes = [root, root + 4, root + 8, root + 10] - return cls(notes) - - @classmethod - def create_augmented_major_seventh(cls, root: Note) -> 'Chord': - """Create an augmented major 7th chord from a root note (augmented triad + major 7th).""" - notes = [root, root + 4, root + 8, root + 11] - return cls(notes) - - -class ChordProgression: - def __init__(self, chords: List[Chord]): - self.chords = chords - self._calculate_start_time() - self._calculate_duration() - - def __str__(self) -> str: - return f"[{', '.join(str(chord) for chord in self.chords)}]" - - def get_progression(self) -> List[Chord]: - return self.chords - - def _calculate_duration(self) -> int: - self.duration = sum(chord._calculate_duration() for chord in self.chords) - return self.duration - - def _calculate_start_time(self) -> int: - self.time = min(chord._calculate_start_time() for chord in self.chords) if self.chords else 0 - return self.time - - def __eq__(self, other) -> bool: - return self.chords == other.chords - - def add_chord(self, chord: Chord) -> None: - """ - Add a chord (simultaneous notes) to the track. - - :param chord: A Chord object. - """ - self.chords.append(chord) - self._calculate_duration() - self._calculate_start_time() - - @classmethod - def from_roman_numerals( - cls, - key: Key, - progression_string: str, - octave: int = 4, - duration: int = 480, - time_per_chord: int = 0 - ): - """ - Create a chord progression from Roman numeral notation. - - Args: - key: The key for the progression (e.g., Key("C", "major")) - progression_string: Dash-separated Roman numerals (e.g., "I-V-vi-IV") - octave: Base octave for the chords (default 4) - duration: Duration of each note in ticks (default 480) - time_per_chord: Time between chord starts in ticks (default 0) - - Returns: - ChordProgression containing the parsed chords. - """ - roman_numerals = progression_string.split('-') - chords = [] - current_time = 0 - - for rn_str in roman_numerals: - # Use native parser to get MIDI pitches - pitches = get_chord_pitches(key.name, key.mode, rn_str, octave=octave) - - notes = [] - for midi_pitch in pitches: - notes.append(Note( - pitch=midi_pitch, - velocity=64, - duration=duration, - time=current_time - )) - - if notes: - chords.append(Chord(notes)) - current_time += time_per_chord - - return cls(chords) - - -class ArpeggioPattern(Enum): - ASCENDING = "ascending" - DESCENDING = "descending" - ALTERNATING = "alternating" - - -class Arpeggio(Chord): - def __init__(self, notes: List[Note], delay: int = 0, pattern: ArpeggioPattern = ArpeggioPattern.ASCENDING, loops: int = 1): - """ - :param root_note: The root note of the arpeggio. - :param delay: The delay between each note in the arpeggio. - """ - super().__init__(notes) - self.delay = delay - self.pattern = pattern - self.loops = loops - - def get_notes(self) -> List[Note]: - return self.notes - - def get_sequential_notes(self) -> List[Note]: - """ - Get the sequential notes of the arpeggio based on the pattern, delay, and looping. - - Returns: - A list of notes representing the arpeggio. - """ - sequential_notes = [] - for loop in range(self.loops): - if self.pattern == ArpeggioPattern.ASCENDING: - notes = self.notes - elif self.pattern == ArpeggioPattern.DESCENDING: - notes = list(reversed(self.notes)) - elif self.pattern == ArpeggioPattern.ALTERNATING: - notes = self.notes if loop % 2 == 0 else list(reversed(self.notes)) - - for i, note in enumerate(notes): - # Add an offset to the time for the second and subsequent loops - time_offset = loop * len(notes) * self.delay - time = note.time + time_offset if i == 0 else self.delay * i + time_offset - new_note = Note(note.pitch, note.velocity, note.duration, time) - sequential_notes.append(new_note) - - return sequential_notes diff --git a/midigen/compiler/midi_compiler.py b/midigen/compiler/midi_compiler.py index 60e8b30..8eef640 100644 --- a/midigen/compiler/midi_compiler.py +++ b/midigen/compiler/midi_compiler.py @@ -38,7 +38,8 @@ from midigen.midigen import MidiGen from midigen.theory.key import Key from midigen.theory.note import Note -from midigen.chord import Chord, ChordProgression +from midigen.composition.chord import Chord +from midigen.composition.progression import ChordProgression from midigen.channel_pool import ChannelPool, ChannelExhaustedError from midigen.instruments import INSTRUMENT_MAP diff --git a/midigen/composition/__init__.py b/midigen/composition/__init__.py index 672a684..32212e8 100644 --- a/midigen/composition/__init__.py +++ b/midigen/composition/__init__.py @@ -1 +1,5 @@ """Composition layer — depends only on theory/.""" + +from midigen.composition.chord import Chord, CHORD_TYPES +from midigen.composition.progression import ChordProgression +from midigen.composition.arpeggio import Arpeggio, ArpeggioPattern diff --git a/midigen/composition/arpeggio.py b/midigen/composition/arpeggio.py new file mode 100644 index 0000000..cd0dcf4 --- /dev/null +++ b/midigen/composition/arpeggio.py @@ -0,0 +1,40 @@ +from typing import List +from enum import Enum +from midigen.theory.note import Note +from midigen.composition.chord import Chord + + +class ArpeggioPattern(Enum): + ASCENDING = "ascending" + DESCENDING = "descending" + ALTERNATING = "alternating" + + +class Arpeggio(Chord): + def __init__(self, notes: List[Note], delay: int = 0, pattern: ArpeggioPattern = ArpeggioPattern.ASCENDING, loops: int = 1): + super().__init__(notes) + self.delay = delay + self.pattern = pattern + self.loops = loops + + def get_notes(self) -> List[Note]: + return self.notes + + def get_sequential_notes(self) -> List[Note]: + """Get the sequential notes of the arpeggio based on the pattern, delay, and looping.""" + sequential_notes = [] + for loop in range(self.loops): + if self.pattern == ArpeggioPattern.ASCENDING: + notes = self.notes + elif self.pattern == ArpeggioPattern.DESCENDING: + notes = list(reversed(self.notes)) + elif self.pattern == ArpeggioPattern.ALTERNATING: + notes = self.notes if loop % 2 == 0 else list(reversed(self.notes)) + + for i, note in enumerate(notes): + time_offset = loop * len(notes) * self.delay + time = note.time + time_offset if i == 0 else self.delay * i + time_offset + new_note = Note(note.pitch, note.velocity, note.duration, time) + sequential_notes.append(new_note) + + return sequential_notes diff --git a/midigen/composition/chord.py b/midigen/composition/chord.py new file mode 100644 index 0000000..30eb739 --- /dev/null +++ b/midigen/composition/chord.py @@ -0,0 +1,260 @@ +from typing import List +from midigen.theory.note import Note +from midigen.theory.key import KEY_MAP, Key + + +CHORD_TYPES = { + "major_triad": (0, 4, 7), + "minor_triad": (0, 3, 7), + "dominant_seventh": (0, 4, 7, 10), + "major_seventh": (0, 4, 7, 11), + "minor_seventh": (0, 3, 7, 10), + "half_diminished_seventh": (0, 3, 6, 10), + "diminished_seventh": (0, 3, 6, 9), + "minor_ninth": (0, 3, 7, 10, 14), + "major_ninth": (0, 4, 7, 11, 14), + "dominant_ninth": (0, 4, 7, 10, 14), + "sus2": (0, 2, 7), + "sus4": (0, 5, 7), + "augmented": (0, 4, 8), + "diminished": (0, 3, 6), + "major_sixth": (0, 4, 7, 9), + "minor_sixth": (0, 3, 7, 9), + "dominant_eleventh": (0, 4, 7, 10, 14, 17), + "major_eleventh": (0, 4, 7, 11, 14, 17), + "minor_eleventh": (0, 3, 7, 10, 14, 17), + "dominant_thirteenth": (0, 4, 7, 10, 14, 17, 21), + "major_thirteenth": (0, 4, 7, 11, 14, 17, 21), + "minor_thirteenth": (0, 3, 7, 10, 14, 17, 21), + "add9": (0, 4, 7, 14), + "minor_add9": (0, 3, 7, 14), + "add11": (0, 4, 7, 17), + "augmented_seventh": (0, 4, 8, 10), + "augmented_major_seventh": (0, 4, 8, 11), +} + + +class Chord: + def __init__(self, notes: List[Note]): + self.notes = notes + self.root = self.get_root() + self._calculate_start_time() + self._calculate_duration() + + def __str__(self) -> str: + return f"[{', '.join(str(note) for note in self.notes)}]" + + def _calculate_start_time(self) -> int: + self.time = min(note.time for note in self.notes) if self.notes else 0 + return self.time + + def _calculate_duration(self) -> int: + if not self.notes: + return 0 + earliest_start_time = min(note.time for note in self.notes) + latest_end_time = max(note.time + note.duration for note in self.notes) + self.duration = latest_end_time - earliest_start_time + return self.duration + + def add_note(self, note: Note) -> None: + self.notes.append(note) + self._calculate_duration() + self._calculate_start_time() + + def get_chord(self) -> List[Note]: + return self.notes + + def get_root(self) -> Note: + if self.notes: + self.root = self.notes[0] + return self.root + return None + + # Instance voicing methods (return List[Note] relative to self.root) + + def major_triad(self) -> List[Note]: + return [self.root, self.root + 4, self.root + 7] + + def minor_triad(self) -> List[Note]: + return [self.root, self.root + 3, self.root + 7] + + def dominant_seventh(self) -> List[Note]: + return self.major_triad() + [self.root + 10] + + def major_seventh(self) -> List[Note]: + return self.major_triad() + [self.root + 11] + + def minor_seventh(self) -> List[Note]: + return self.minor_triad() + [self.root + 10] + + def half_diminished_seventh(self) -> List[Note]: + return [self.root, self.root + 3, self.root + 6, self.root + 10] + + def diminished_seventh(self) -> List[Note]: + return [self.root, self.root + 3, self.root + 6, self.root + 9] + + def minor_ninth(self) -> List[Note]: + return self.minor_seventh() + [self.root + 14] + + def major_ninth(self) -> List[Note]: + return self.major_seventh() + [self.root + 14] + + def dominant_ninth(self) -> List[Note]: + return self.dominant_seventh() + [self.root + 14] + + # Data-driven factory + + @classmethod + def build(cls, chord_type: str, root: Note) -> 'Chord': + """Build a chord from a type name and root note. + + Args: + chord_type: Key from CHORD_TYPES (e.g. "major_triad", "sus2", "dominant_ninth") + root: The root Note for the chord. + + Returns: + A new Chord instance. + + Raises: + KeyError: If chord_type is not in CHORD_TYPES. + """ + intervals = CHORD_TYPES[chord_type] + notes = [root + interval for interval in intervals] + return cls(notes) + + # Named factory wrappers (backwards-compatible API) + + @classmethod + def create_major_triad(cls, root: Note) -> 'Chord': + """Create a major triad chord from a root note.""" + return cls.build("major_triad", root) + + @classmethod + def create_minor_triad(cls, root: Note) -> 'Chord': + """Create a minor triad chord from a root note.""" + return cls.build("minor_triad", root) + + @classmethod + def create_dominant_seventh(cls, root: Note) -> 'Chord': + """Create a dominant seventh chord from a root note.""" + return cls.build("dominant_seventh", root) + + @classmethod + def create_major_seventh(cls, root: Note) -> 'Chord': + """Create a major seventh chord from a root note.""" + return cls.build("major_seventh", root) + + @classmethod + def create_minor_seventh(cls, root: Note) -> 'Chord': + """Create a minor seventh chord from a root note.""" + return cls.build("minor_seventh", root) + + @classmethod + def create_half_diminished_seventh(cls, root: Note) -> 'Chord': + """Create a half-diminished seventh chord from a root note.""" + return cls.build("half_diminished_seventh", root) + + @classmethod + def create_diminished_seventh(cls, root: Note) -> 'Chord': + """Create a diminished seventh chord from a root note.""" + return cls.build("diminished_seventh", root) + + @classmethod + def create_minor_ninth(cls, root: Note) -> 'Chord': + """Create a minor ninth chord from a root note.""" + return cls.build("minor_ninth", root) + + @classmethod + def create_major_ninth(cls, root: Note) -> 'Chord': + """Create a major ninth chord from a root note.""" + return cls.build("major_ninth", root) + + @classmethod + def create_dominant_ninth(cls, root: Note) -> 'Chord': + """Create a dominant ninth chord from a root note.""" + return cls.build("dominant_ninth", root) + + @classmethod + def create_sus2(cls, root: Note) -> 'Chord': + """Create a suspended 2nd chord from a root note.""" + return cls.build("sus2", root) + + @classmethod + def create_sus4(cls, root: Note) -> 'Chord': + """Create a suspended 4th chord from a root note.""" + return cls.build("sus4", root) + + @classmethod + def create_augmented(cls, root: Note) -> 'Chord': + """Create an augmented triad from a root note.""" + return cls.build("augmented", root) + + @classmethod + def create_diminished(cls, root: Note) -> 'Chord': + """Create a diminished triad from a root note.""" + return cls.build("diminished", root) + + @classmethod + def create_major_sixth(cls, root: Note) -> 'Chord': + """Create a major 6th chord from a root note.""" + return cls.build("major_sixth", root) + + @classmethod + def create_minor_sixth(cls, root: Note) -> 'Chord': + """Create a minor 6th chord from a root note.""" + return cls.build("minor_sixth", root) + + @classmethod + def create_dominant_eleventh(cls, root: Note) -> 'Chord': + """Create a dominant 11th chord from a root note.""" + return cls.build("dominant_eleventh", root) + + @classmethod + def create_major_eleventh(cls, root: Note) -> 'Chord': + """Create a major 11th chord from a root note.""" + return cls.build("major_eleventh", root) + + @classmethod + def create_minor_eleventh(cls, root: Note) -> 'Chord': + """Create a minor 11th chord from a root note.""" + return cls.build("minor_eleventh", root) + + @classmethod + def create_dominant_thirteenth(cls, root: Note) -> 'Chord': + """Create a dominant 13th chord from a root note.""" + return cls.build("dominant_thirteenth", root) + + @classmethod + def create_major_thirteenth(cls, root: Note) -> 'Chord': + """Create a major 13th chord from a root note.""" + return cls.build("major_thirteenth", root) + + @classmethod + def create_minor_thirteenth(cls, root: Note) -> 'Chord': + """Create a minor 13th chord from a root note.""" + return cls.build("minor_thirteenth", root) + + @classmethod + def create_add9(cls, root: Note) -> 'Chord': + """Create an add9 chord from a root note.""" + return cls.build("add9", root) + + @classmethod + def create_minor_add9(cls, root: Note) -> 'Chord': + """Create a minor add9 chord from a root note.""" + return cls.build("minor_add9", root) + + @classmethod + def create_add11(cls, root: Note) -> 'Chord': + """Create an add11 chord from a root note.""" + return cls.build("add11", root) + + @classmethod + def create_augmented_seventh(cls, root: Note) -> 'Chord': + """Create an augmented 7th chord from a root note.""" + return cls.build("augmented_seventh", root) + + @classmethod + def create_augmented_major_seventh(cls, root: Note) -> 'Chord': + """Create an augmented major 7th chord from a root note.""" + return cls.build("augmented_major_seventh", root) diff --git a/midigen/composition/progression.py b/midigen/composition/progression.py new file mode 100644 index 0000000..96cd186 --- /dev/null +++ b/midigen/composition/progression.py @@ -0,0 +1,77 @@ +from typing import List +from midigen.theory.note import Note +from midigen.theory.key import Key +from midigen.theory.roman import get_chord_pitches +from midigen.composition.chord import Chord + + +class ChordProgression: + def __init__(self, chords: List[Chord]): + self.chords = chords + self._calculate_start_time() + self._calculate_duration() + + def __str__(self) -> str: + return f"[{', '.join(str(chord) for chord in self.chords)}]" + + def get_progression(self) -> List[Chord]: + return self.chords + + def _calculate_duration(self) -> int: + self.duration = sum(chord._calculate_duration() for chord in self.chords) + return self.duration + + def _calculate_start_time(self) -> int: + self.time = min(chord._calculate_start_time() for chord in self.chords) if self.chords else 0 + return self.time + + def __eq__(self, other) -> bool: + return self.chords == other.chords + + def add_chord(self, chord: Chord) -> None: + self.chords.append(chord) + self._calculate_duration() + self._calculate_start_time() + + @classmethod + def from_roman_numerals( + cls, + key: Key, + progression_string: str, + octave: int = 4, + duration: int = 480, + time_per_chord: int = 0 + ): + """Create a chord progression from Roman numeral notation. + + Args: + key: The key for the progression (e.g., Key("C", "major")) + progression_string: Dash-separated Roman numerals (e.g., "I-V-vi-IV") + octave: Base octave for the chords (default 4) + duration: Duration of each note in ticks (default 480) + time_per_chord: Time between chord starts in ticks (default 0) + + Returns: + ChordProgression containing the parsed chords. + """ + roman_numerals = progression_string.split('-') + chords = [] + current_time = 0 + + for rn_str in roman_numerals: + pitches = get_chord_pitches(key.name, key.mode, rn_str, octave=octave) + + notes = [] + for midi_pitch in pitches: + notes.append(Note( + pitch=midi_pitch, + velocity=64, + duration=duration, + time=current_time + )) + + if notes: + chords.append(Chord(notes)) + current_time += time_per_chord + + return cls(chords) diff --git a/midigen/tests/test_chord.py b/midigen/tests/test_chord.py index 7c7b89d..3a23d93 100644 --- a/midigen/tests/test_chord.py +++ b/midigen/tests/test_chord.py @@ -1,6 +1,8 @@ from midigen.theory.note import Note from midigen.theory.key import KEY_MAP -from midigen.chord import Chord, ChordProgression, Arpeggio, ArpeggioPattern +from midigen.composition.chord import Chord +from midigen.composition.progression import ChordProgression +from midigen.composition.arpeggio import Arpeggio, ArpeggioPattern import unittest diff --git a/midigen/tests/test_track.py b/midigen/tests/test_track.py index 4617644..5d01c15 100644 --- a/midigen/tests/test_track.py +++ b/midigen/tests/test_track.py @@ -1,7 +1,8 @@ from midigen.midigen import MidiGen from midigen.theory.note import Note from midigen.theory.key import KEY_MAP -from midigen.chord import Chord, Arpeggio +from midigen.composition.chord import Chord +from midigen.composition.arpeggio import Arpeggio from midigen.track import MAX_MIDI_TICKS import unittest diff --git a/midigen/track.py b/midigen/track.py index cf887a6..cfd9dbc 100644 --- a/midigen/track.py +++ b/midigen/track.py @@ -3,7 +3,9 @@ import warnings -from midigen.chord import Chord, ChordProgression, Arpeggio +from midigen.composition.chord import Chord +from midigen.composition.progression import ChordProgression +from midigen.composition.arpeggio import Arpeggio from midigen.theory.key import Key from midigen.theory.note import Note from midigen.drums import DrumKit From 476622e03be90f66332da125134a799aee4d2748 Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:50:30 -0400 Subject: [PATCH 04/11] move melody, drums, section to composition/ with Section validation Section.__init__ now validates chord progression strings at construction time via parse_roman_numeral(). Raises ValueError on malformed input instead of failing deep in compilation. --- midigen/__init__.py | 6 +++--- midigen/composition/__init__.py | 3 +++ midigen/{ => composition}/drums.py | 0 midigen/{ => composition}/melody.py | 0 midigen/composition/section.py | 28 ++++++++++++++++++++++++++++ midigen/section.py | 5 ----- midigen/song.py | 2 +- midigen/tests/test_drums.py | 2 +- midigen/track.py | 2 +- 9 files changed, 37 insertions(+), 11 deletions(-) rename midigen/{ => composition}/drums.py (100%) rename midigen/{ => composition}/melody.py (100%) create mode 100644 midigen/composition/section.py delete mode 100644 midigen/section.py diff --git a/midigen/__init__.py b/midigen/__init__.py index eb4e0de..3d82b0a 100644 --- a/midigen/__init__.py +++ b/midigen/__init__.py @@ -7,9 +7,9 @@ from .composition.chord import Chord, CHORD_TYPES from .composition.progression import ChordProgression from .composition.arpeggio import Arpeggio, ArpeggioPattern -from .drums import DrumKit, Drum -from .melody import Melody -from .section import Section +from .composition.drums import DrumKit, Drum +from .composition.melody import Melody +from .composition.section import Section from .track import Track from .channel_pool import ChannelPool, ChannelExhaustedError diff --git a/midigen/composition/__init__.py b/midigen/composition/__init__.py index 32212e8..ff0994d 100644 --- a/midigen/composition/__init__.py +++ b/midigen/composition/__init__.py @@ -3,3 +3,6 @@ from midigen.composition.chord import Chord, CHORD_TYPES from midigen.composition.progression import ChordProgression from midigen.composition.arpeggio import Arpeggio, ArpeggioPattern +from midigen.composition.melody import Melody +from midigen.composition.drums import DrumKit, Drum, GM1_DRUM_MAP +from midigen.composition.section import Section diff --git a/midigen/drums.py b/midigen/composition/drums.py similarity index 100% rename from midigen/drums.py rename to midigen/composition/drums.py diff --git a/midigen/melody.py b/midigen/composition/melody.py similarity index 100% rename from midigen/melody.py rename to midigen/composition/melody.py diff --git a/midigen/composition/section.py b/midigen/composition/section.py new file mode 100644 index 0000000..02667d5 --- /dev/null +++ b/midigen/composition/section.py @@ -0,0 +1,28 @@ +from midigen.theory.roman import parse_roman_numeral + + +class Section: + def __init__(self, name: str, length: int, chord_progression: str): + self.name = name + self.length = length + self.chord_progression = chord_progression + self._validate_progression() + + def _validate_progression(self): + if not self.chord_progression or not self.chord_progression.strip(): + raise ValueError("Chord progression cannot be empty") + + tokens = self.chord_progression.split('-') + for token in tokens: + token = token.strip() + if not token: + raise ValueError( + f"Invalid chord progression '{self.chord_progression}': " + "contains empty token (double dash or trailing dash)" + ) + try: + parse_roman_numeral(token) + except ValueError as e: + raise ValueError( + f"Invalid chord progression '{self.chord_progression}': {e}" + ) from e diff --git a/midigen/section.py b/midigen/section.py deleted file mode 100644 index 42a155c..0000000 --- a/midigen/section.py +++ /dev/null @@ -1,5 +0,0 @@ -class Section: - def __init__(self, name: str, length: int, chord_progression: str): - self.name = name - self.length = length - self.chord_progression = chord_progression diff --git a/midigen/song.py b/midigen/song.py index 5cd4c9a..799d3a8 100644 --- a/midigen/song.py +++ b/midigen/song.py @@ -28,7 +28,7 @@ from typing import List, Set from midigen.theory.key import Key -from midigen.section import Section +from midigen.composition.section import Section class Song: diff --git a/midigen/tests/test_drums.py b/midigen/tests/test_drums.py index c8c9931..d00a9aa 100644 --- a/midigen/tests/test_drums.py +++ b/midigen/tests/test_drums.py @@ -1,5 +1,5 @@ import unittest -from midigen.drums import DrumKit, Drum, GM1_DRUM_MAP +from midigen.composition.drums import DrumKit, Drum, GM1_DRUM_MAP from midigen.theory.note import Note diff --git a/midigen/track.py b/midigen/track.py index cfd9dbc..b5af9b0 100644 --- a/midigen/track.py +++ b/midigen/track.py @@ -8,7 +8,7 @@ from midigen.composition.arpeggio import Arpeggio from midigen.theory.key import Key from midigen.theory.note import Note -from midigen.drums import DrumKit +from midigen.composition.drums import DrumKit MAX_MIDI_TICKS = 32767 # Maximum value for a 15-bit signed integer From e776747151121a08a649b6a82dce99985ccd15eb Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:52:05 -0400 Subject: [PATCH 05/11] move track, channel_pool, instruments to protocol/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track now uses TYPE_CHECKING for composition/ imports — zero runtime dependency on composition layer. Only Note and Key imported at runtime (for isinstance checks). --- midigen/__init__.py | 6 +++--- midigen/compiler/midi_compiler.py | 4 ++-- midigen/midigen.py | 2 +- midigen/protocol/__init__.py | 4 ++++ midigen/{ => protocol}/channel_pool.py | 0 midigen/{ => protocol}/instruments.py | 0 midigen/{ => protocol}/track.py | 15 +++++++++------ midigen/song.py | 2 +- midigen/tests/test_channel_pool.py | 2 +- midigen/tests/test_song.py | 4 ++-- midigen/tests/test_track.py | 2 +- 11 files changed, 24 insertions(+), 17 deletions(-) rename midigen/{ => protocol}/channel_pool.py (100%) rename midigen/{ => protocol}/instruments.py (100%) rename midigen/{ => protocol}/track.py (97%) diff --git a/midigen/__init__.py b/midigen/__init__.py index 3d82b0a..a6a22e9 100644 --- a/midigen/__init__.py +++ b/midigen/__init__.py @@ -11,9 +11,9 @@ from .composition.melody import Melody from .composition.section import Section -from .track import Track -from .channel_pool import ChannelPool, ChannelExhaustedError -from .instruments import INSTRUMENT_MAP +from .protocol.track import Track +from .protocol.channel_pool import ChannelPool, ChannelExhaustedError +from .protocol.instruments import INSTRUMENT_MAP from .midigen import MidiGen from .song import Song diff --git a/midigen/compiler/midi_compiler.py b/midigen/compiler/midi_compiler.py index 8eef640..f550fbb 100644 --- a/midigen/compiler/midi_compiler.py +++ b/midigen/compiler/midi_compiler.py @@ -40,8 +40,8 @@ from midigen.theory.note import Note from midigen.composition.chord import Chord from midigen.composition.progression import ChordProgression -from midigen.channel_pool import ChannelPool, ChannelExhaustedError -from midigen.instruments import INSTRUMENT_MAP +from midigen.protocol.channel_pool import ChannelPool, ChannelExhaustedError +from midigen.protocol.instruments import INSTRUMENT_MAP # Default timing constants diff --git a/midigen/midigen.py b/midigen/midigen.py index 97a4f94..e122bbd 100644 --- a/midigen/midigen.py +++ b/midigen/midigen.py @@ -2,7 +2,7 @@ import os from mido import MidiFile from midigen.theory.key import Key -from midigen.track import Track +from midigen.protocol.track import Track from pathlib import Path diff --git a/midigen/protocol/__init__.py b/midigen/protocol/__init__.py index 5689da0..d8049f2 100644 --- a/midigen/protocol/__init__.py +++ b/midigen/protocol/__init__.py @@ -1 +1,5 @@ """MIDI protocol layer — depends on theory/, TYPE_CHECKING only for composition/.""" + +from midigen.protocol.track import Track, MAX_MIDI_TICKS +from midigen.protocol.channel_pool import ChannelPool, ChannelExhaustedError +from midigen.protocol.instruments import INSTRUMENT_MAP diff --git a/midigen/channel_pool.py b/midigen/protocol/channel_pool.py similarity index 100% rename from midigen/channel_pool.py rename to midigen/protocol/channel_pool.py diff --git a/midigen/instruments.py b/midigen/protocol/instruments.py similarity index 100% rename from midigen/instruments.py rename to midigen/protocol/instruments.py diff --git a/midigen/track.py b/midigen/protocol/track.py similarity index 97% rename from midigen/track.py rename to midigen/protocol/track.py index b5af9b0..cee55a4 100644 --- a/midigen/track.py +++ b/midigen/protocol/track.py @@ -1,14 +1,17 @@ +from __future__ import annotations + from mido import MidiTrack, Message, MetaMessage, bpm2tempo -from typing import List, Tuple +from typing import List, Tuple, TYPE_CHECKING import warnings - -from midigen.composition.chord import Chord -from midigen.composition.progression import ChordProgression -from midigen.composition.arpeggio import Arpeggio from midigen.theory.key import Key from midigen.theory.note import Note -from midigen.composition.drums import DrumKit + +if TYPE_CHECKING: + from midigen.composition.chord import Chord + from midigen.composition.progression import ChordProgression + from midigen.composition.arpeggio import Arpeggio + from midigen.composition.drums import DrumKit MAX_MIDI_TICKS = 32767 # Maximum value for a 15-bit signed integer diff --git a/midigen/song.py b/midigen/song.py index 799d3a8..e0abf58 100644 --- a/midigen/song.py +++ b/midigen/song.py @@ -106,7 +106,7 @@ def add_instrument(self, name: str) -> "Song": Raises: ValueError: If the instrument name is not valid. """ - from midigen.instruments import INSTRUMENT_MAP + from midigen.protocol.instruments import INSTRUMENT_MAP if name not in INSTRUMENT_MAP: raise ValueError(f"Instrument '{name}' not found in INSTRUMENT_MAP.") diff --git a/midigen/tests/test_channel_pool.py b/midigen/tests/test_channel_pool.py index bf2620f..56f2e62 100644 --- a/midigen/tests/test_channel_pool.py +++ b/midigen/tests/test_channel_pool.py @@ -1,5 +1,5 @@ import unittest -from midigen.channel_pool import ChannelPool, ChannelExhaustedError +from midigen.protocol.channel_pool import ChannelPool, ChannelExhaustedError class TestChannelPool(unittest.TestCase): diff --git a/midigen/tests/test_song.py b/midigen/tests/test_song.py index b46ab08..ab17c53 100644 --- a/midigen/tests/test_song.py +++ b/midigen/tests/test_song.py @@ -8,8 +8,8 @@ import unittest import warnings from midigen import Song, Section, Key, MidiCompiler -from midigen.channel_pool import ChannelExhaustedError -from midigen.instruments import INSTRUMENT_MAP +from midigen.protocol.channel_pool import ChannelExhaustedError +from midigen.protocol.instruments import INSTRUMENT_MAP class TestSong(unittest.TestCase): diff --git a/midigen/tests/test_track.py b/midigen/tests/test_track.py index 5d01c15..bca9be2 100644 --- a/midigen/tests/test_track.py +++ b/midigen/tests/test_track.py @@ -3,7 +3,7 @@ from midigen.theory.key import KEY_MAP from midigen.composition.chord import Chord from midigen.composition.arpeggio import Arpeggio -from midigen.track import MAX_MIDI_TICKS +from midigen.protocol.track import MAX_MIDI_TICKS import unittest From 1c6200244a129253692038909e6346b03be8b399 Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:55:02 -0400 Subject: [PATCH 06/11] decouple Song from MidiCompiler, move api/ layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Song is now a pure data container with zero compiler references. All deprecated methods removed (generate, save, midigen property, available_channels). MidiCompiler imports Song at top level — no more late-import circular dependency hack. --- midigen/__init__.py | 6 +- midigen/api/__init__.py | 4 + .../midi_compiler.py => api/compiler.py} | 8 +- midigen/{ => api}/midigen.py | 0 midigen/api/song.py | 71 ++++++ midigen/compiler/__init__.py | 10 - midigen/song.py | 210 ------------------ midigen/tests/test_golden_master.py | 8 +- midigen/tests/test_integration.py | 2 +- midigen/tests/test_midigen.py | 2 +- midigen/tests/test_song.py | 40 ---- midigen/tests/test_track.py | 2 +- 12 files changed, 88 insertions(+), 275 deletions(-) rename midigen/{compiler/midi_compiler.py => api/compiler.py} (98%) rename midigen/{ => api}/midigen.py (100%) create mode 100644 midigen/api/song.py delete mode 100644 midigen/compiler/__init__.py delete mode 100644 midigen/song.py diff --git a/midigen/__init__.py b/midigen/__init__.py index a6a22e9..373cb1a 100644 --- a/midigen/__init__.py +++ b/midigen/__init__.py @@ -15,6 +15,6 @@ from .protocol.channel_pool import ChannelPool, ChannelExhaustedError from .protocol.instruments import INSTRUMENT_MAP -from .midigen import MidiGen -from .song import Song -from .compiler import MidiCompiler +from .api.midigen import MidiGen +from .api.song import Song +from .api.compiler import MidiCompiler diff --git a/midigen/api/__init__.py b/midigen/api/__init__.py index 5a49135..33efe8e 100644 --- a/midigen/api/__init__.py +++ b/midigen/api/__init__.py @@ -1 +1,5 @@ """High-level API layer — depends on all layers.""" + +from midigen.api.song import Song +from midigen.api.midigen import MidiGen +from midigen.api.compiler import MidiCompiler diff --git a/midigen/compiler/midi_compiler.py b/midigen/api/compiler.py similarity index 98% rename from midigen/compiler/midi_compiler.py rename to midigen/api/compiler.py index f550fbb..e861caa 100644 --- a/midigen/compiler/midi_compiler.py +++ b/midigen/api/compiler.py @@ -35,7 +35,8 @@ from typing import Dict, List, Optional -from midigen.midigen import MidiGen +from midigen.api.midigen import MidiGen +from midigen.api.song import Song from midigen.theory.key import Key from midigen.theory.note import Note from midigen.composition.chord import Chord @@ -346,8 +347,3 @@ def get_track(self, instrument_name: str): if track_index is not None: return self._midigen.tracks[track_index] return None - - -# Import Song here to avoid circular imports -# This is at the bottom because Song is imported for type hints only -from midigen.song import Song # noqa: E402 diff --git a/midigen/midigen.py b/midigen/api/midigen.py similarity index 100% rename from midigen/midigen.py rename to midigen/api/midigen.py diff --git a/midigen/api/song.py b/midigen/api/song.py new file mode 100644 index 0000000..1ba237d --- /dev/null +++ b/midigen/api/song.py @@ -0,0 +1,71 @@ +""" +Song - High-level musical composition container. + +The Song class represents the musical intent of a composition: +- Key and tempo +- Sections (verse, chorus, bridge, etc.) +- Instrument definitions + +For MIDI generation, use the MidiCompiler: + + >>> from midigen import Song, Section, Key, MidiCompiler + >>> + >>> song = Song(key=Key("C", "major"), tempo=120) + >>> song.add_section(Section("Verse", 8, "I-V-vi-IV")) + >>> song.add_instrument("Acoustic Grand Piano") + >>> + >>> compiler = MidiCompiler(song) + >>> compiler.compile() + >>> compiler.save("output.mid") +""" + +from typing import List, Set + +from midigen.theory.key import Key +from midigen.composition.section import Section +from midigen.protocol.instruments import INSTRUMENT_MAP + + +class Song: + """ + High-level song composition container. + + A Song is a pure data container that holds: + - Musical metadata (key, tempo) + - Sections (verse, chorus, bridge, etc.) + - Instrument definitions (what instruments to use) + + The actual MIDI generation is handled by MidiCompiler. + + Example: + >>> song = Song(key=Key("C", "major"), tempo=120) + >>> song.add_section(Section("Verse", 8, "I-V-vi-IV")) + >>> song.add_instrument("Acoustic Grand Piano") + >>> + >>> from midigen.api.compiler import MidiCompiler + >>> compiler = MidiCompiler(song) + >>> compiler.compile().save("output.mid") + """ + + def __init__(self, tempo: int = 120, key: Key = None): + self.tempo = tempo + self.key = key if key else Key("C") + self.sections: List[Section] = [] + self.instruments: Set[str] = set() + + def add_section(self, section: Section) -> "Song": + """Add a section to the song. Returns self for chaining.""" + self.sections.append(section) + return self + + def add_instrument(self, name: str) -> "Song": + """Register an instrument. Raises ValueError if name is invalid.""" + if name not in INSTRUMENT_MAP: + raise ValueError(f"Instrument '{name}' not found in INSTRUMENT_MAP.") + self.instruments.add(name) + return self + + def add_drums(self, name: str = "Drums") -> "Song": + """Register a drum track. Returns self for chaining.""" + self.instruments.add(name) + return self diff --git a/midigen/compiler/__init__.py b/midigen/compiler/__init__.py deleted file mode 100644 index 82af8f5..0000000 --- a/midigen/compiler/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Compiler package for transforming musical intent into MIDI protocol. - -This package bridges the gap between high-level musical concepts -(songs, sections, progressions) and low-level MIDI implementation. -""" - -from .midi_compiler import MidiCompiler - -__all__ = ["MidiCompiler"] diff --git a/midigen/song.py b/midigen/song.py deleted file mode 100644 index e0abf58..0000000 --- a/midigen/song.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -Song - High-level musical composition container. - -The Song class represents the musical intent of a composition: -- Key and tempo -- Sections (verse, chorus, bridge, etc.) -- Instrument definitions - -For MIDI generation, use the MidiCompiler: - - >>> from midigen import Song, Section, Key - >>> from midigen.compiler import MidiCompiler - >>> - >>> song = Song(key=Key("C", "major"), tempo=120) - >>> song.add_section(Section("Verse", 8, "I-V-vi-IV")) - >>> song.add_instrument("Acoustic Grand Piano") - >>> - >>> compiler = MidiCompiler(song) - >>> compiler.compile() - >>> compiler.save("output.mid") - -For backward compatibility, Song still supports the legacy API: - >>> song.generate("Acoustic Grand Piano") # Deprecated - >>> song.save("output.mid") # Deprecated -""" - -import warnings -from typing import List, Set - -from midigen.theory.key import Key -from midigen.composition.section import Section - - -class Song: - """ - High-level song composition container. - - A Song is a pure data container that holds: - - Musical metadata (key, tempo) - - Sections (verse, chorus, bridge, etc.) - - Instrument definitions (what instruments to use) - - The actual MIDI generation is handled by MidiCompiler. - - Example (New API): - >>> song = Song(key=Key("C", "major"), tempo=120) - >>> song.add_section(Section("Verse", 8, "I-V-vi-IV")) - >>> song.add_instrument("Acoustic Grand Piano") - >>> - >>> from midigen.compiler import MidiCompiler - >>> compiler = MidiCompiler(song) - >>> compiler.compile().save("output.mid") - - Example (Legacy API - Deprecated): - >>> song = Song(key=Key("C", "major"), tempo=120) - >>> song.add_section(Section("Verse", 8, "I-V-vi-IV")) - >>> song.add_instrument("Acoustic Grand Piano") - >>> song.generate("Acoustic Grand Piano") - >>> song.save("output.mid") - """ - - def __init__(self, tempo: int = 120, key: Key = None): - """ - Initialize a new Song. - - Args: - tempo: Beats per minute (default 120). - key: The key signature (default C major). - """ - self.tempo = tempo - self.key = key if key else Key("C") - self.sections: List[Section] = [] - self.instruments: Set[str] = set() - - # Lazy-initialized compiler for backward compatibility - self._compiler = None - - def add_section(self, section: Section) -> "Song": - """ - Add a section to the song. - - Args: - section: A Section object (verse, chorus, etc.). - - Returns: - self for method chaining. - """ - self.sections.append(section) - # Invalidate any existing compiler - self._compiler = None - return self - - def add_instrument(self, name: str) -> "Song": - """ - Register an instrument to be used in the song. - - This method now only records the instrument name. The actual - MIDI channel allocation happens during compilation. - - Args: - name: The instrument name (must be in INSTRUMENT_MAP). - - Returns: - self for method chaining. - - Raises: - ValueError: If the instrument name is not valid. - """ - from midigen.protocol.instruments import INSTRUMENT_MAP - - if name not in INSTRUMENT_MAP: - raise ValueError(f"Instrument '{name}' not found in INSTRUMENT_MAP.") - - self.instruments.add(name) - # Invalidate any existing compiler - self._compiler = None - return self - - def add_drums(self, name: str = "Drums") -> "Song": - """ - Register a drum track. - - Args: - name: A name for the drum track (default: "Drums"). - - Returns: - self for method chaining. - """ - self.instruments.add(name) - # Invalidate any existing compiler - self._compiler = None - return self - - # ========================================================================= - # LEGACY API (Backward Compatibility) - # These methods delegate to MidiCompiler but are deprecated. - # ========================================================================= - - def _get_compiler(self): - """Get or create the compiler for legacy operations.""" - if self._compiler is None: - from midigen.compiler import MidiCompiler - self._compiler = MidiCompiler(self) - # Pre-register all instruments - for name in self.instruments: - if name == "Drums": - self._compiler.add_drums(name) - else: - self._compiler.add_instrument(name) - return self._compiler - - @property - def midigen(self): - """ - Access the underlying MidiGen object. - - Deprecated: Use MidiCompiler instead. - """ - return self._get_compiler().midigen - - @property - def available_channels(self) -> int: - """ - Number of melodic channels still available. - - Deprecated: Use MidiCompiler.available_channels instead. - """ - return self._get_compiler().available_channels - - def generate(self, instrument_name: str, octave: int = 4, duration: int = 480): - """ - Generate MIDI events for an instrument. - - Deprecated: Use MidiCompiler.compile_instrument() instead. - - Args: - instrument_name: The instrument to generate. - octave: Base octave for chords (default 4). - duration: Duration per chord in ticks (default 480). - """ - warnings.warn( - "Song.generate() is deprecated. Use MidiCompiler instead:\n" - " compiler = MidiCompiler(song)\n" - " compiler.compile_instrument(instrument_name)", - DeprecationWarning, - stacklevel=2 - ) - self._get_compiler().compile_instrument(instrument_name, octave, duration) - - def save(self, filename: str, output_dir: str = None) -> str: - """ - Save the song to a MIDI file. - - Deprecated: Use MidiCompiler.save() instead. - - Args: - filename: The name of the MIDI file. - output_dir: Directory to save the file. - - Returns: - The full path to the saved file. - """ - warnings.warn( - "Song.save() is deprecated. Use MidiCompiler instead:\n" - " compiler = MidiCompiler(song)\n" - " compiler.compile().save(filename)", - DeprecationWarning, - stacklevel=2 - ) - return self._get_compiler().save(filename, output_dir) diff --git a/midigen/tests/test_golden_master.py b/midigen/tests/test_golden_master.py index 32eddd5..3086f4b 100644 --- a/midigen/tests/test_golden_master.py +++ b/midigen/tests/test_golden_master.py @@ -15,7 +15,7 @@ from io import BytesIO from pathlib import Path -from midigen import MidiGen, Song, Section, Key, Note, Chord +from midigen import MidiGen, Song, Section, Key, Note, Chord, MidiCompiler GOLDEN_DIR = Path(__file__).parent / "golden_files" @@ -143,9 +143,11 @@ def test_song_progression_i_v_vi_iv(self): song = Song(key=Key("C", "major"), tempo=120) song.add_section(Section(name="Verse", length=4, chord_progression="I-V-vi-IV")) song.add_instrument("Acoustic Grand Piano") - song.generate("Acoustic Grand Piano") - output = get_midi_bytes(song.midigen) + compiler = MidiCompiler(song) + compiler.compile_instrument("Acoustic Grand Piano") + + output = get_midi_bytes(compiler.midigen) self._compare_or_regenerate( output, "progression_i_v_vi_iv.mid", "I-V-vi-IV progression" ) diff --git a/midigen/tests/test_integration.py b/midigen/tests/test_integration.py index f2a2191..870312e 100644 --- a/midigen/tests/test_integration.py +++ b/midigen/tests/test_integration.py @@ -65,7 +65,7 @@ def test_complete_song_with_drums_chords_and_melody(self): def test_song_class_with_multiple_sections_and_instruments(self): """Test Song class with complex arrangement using MidiCompiler""" - from midigen.compiler import MidiCompiler + from midigen.api.compiler import MidiCompiler song = Song(key=Key("D", "minor"), tempo=110) diff --git a/midigen/tests/test_midigen.py b/midigen/tests/test_midigen.py index d015902..c61dd06 100644 --- a/midigen/tests/test_midigen.py +++ b/midigen/tests/test_midigen.py @@ -1,5 +1,5 @@ import os -from midigen.midigen import MidiGen +from midigen.api.midigen import MidiGen from midigen.theory.note import Note from mido import MidiFile from midigen.theory.key import Key diff --git a/midigen/tests/test_song.py b/midigen/tests/test_song.py index ab17c53..baf8b46 100644 --- a/midigen/tests/test_song.py +++ b/midigen/tests/test_song.py @@ -6,7 +6,6 @@ """ import unittest -import warnings from midigen import Song, Section, Key, MidiCompiler from midigen.protocol.channel_pool import ChannelExhaustedError from midigen.protocol.instruments import INSTRUMENT_MAP @@ -256,44 +255,5 @@ def test_multi_section_timing(self): self.assertEqual(len(verse_notes), 48) # 16 chords * 3 notes -class TestLegacyAPI(unittest.TestCase): - """Tests for backward compatibility with legacy Song API.""" - - def test_legacy_generate(self): - """Test legacy Song.generate() still works.""" - song = Song(key=Key("C", "major")) - song.add_section(Section(name="Verse", length=4, chord_progression="I-V-vi-IV")) - song.add_instrument("Acoustic Grand Piano") - - # Suppress the deprecation warning for this test - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - song.generate("Acoustic Grand Piano") - - track = song.midigen.tracks[1] # Track 0 is default, 1 is piano - # With section length enforcement: 4 bars * 4 beats = 16 chords - # 16 chords * 3 notes = 48 notes - self.assertEqual(len(track.notes), 48) - - def test_legacy_available_channels(self): - """Test legacy available_channels property.""" - song = Song() - song.add_instrument("Acoustic Grand Piano") - song.add_instrument("Acoustic Bass") - - # This triggers compiler creation and channel allocation - available = song.available_channels - self.assertEqual(available, 13) - - def test_deprecation_warning_on_generate(self): - """Test that Song.generate() raises deprecation warning.""" - song = Song() - song.add_section(Section("Verse", 4, "I-V-vi-IV")) - song.add_instrument("Acoustic Grand Piano") - - with self.assertWarns(DeprecationWarning): - song.generate("Acoustic Grand Piano") - - if __name__ == "__main__": unittest.main() diff --git a/midigen/tests/test_track.py b/midigen/tests/test_track.py index bca9be2..81f782a 100644 --- a/midigen/tests/test_track.py +++ b/midigen/tests/test_track.py @@ -1,4 +1,4 @@ -from midigen.midigen import MidiGen +from midigen.api.midigen import MidiGen from midigen.theory.note import Note from midigen.theory.key import KEY_MAP from midigen.composition.chord import Chord From 90cddec9497cde607edacff079354c0641ff44b5 Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:57:33 -0400 Subject: [PATCH 07/11] remove deprecated Track.add_note_off_messages Zero DeprecationWarnings in test suite. --- midigen/protocol/track.py | 18 ------------------ midigen/tests/test_integration.py | 10 +++++----- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/midigen/protocol/track.py b/midigen/protocol/track.py index cee55a4..a6291da 100644 --- a/midigen/protocol/track.py +++ b/midigen/protocol/track.py @@ -2,7 +2,6 @@ from mido import MidiTrack, Message, MetaMessage, bpm2tempo from typing import List, Tuple, TYPE_CHECKING -import warnings from midigen.theory.key import Key from midigen.theory.note import Note @@ -221,23 +220,6 @@ def add_note(self, note: Note) -> None: raise TypeError(f"Expected Note object, got {type(note).__name__}") self.notes.append(note) - def add_note_off_messages(self) -> None: - """ - DEPRECATED: This method is no longer needed. - - Use compile() instead, which properly handles note_on/note_off messages - with correct delta timing. - - This method is kept for backward compatibility but does nothing. - """ - warnings.warn( - "add_note_off_messages() is deprecated and has no effect. " - "Use compile() for proper MIDI output with correct delta timing.", - DeprecationWarning, - stacklevel=2 - ) - # No-op: compile() handles note_off messages correctly - def add_chord(self, chord: Chord) -> None: """ Add a chord (simultaneous notes) to the track. diff --git a/midigen/tests/test_integration.py b/midigen/tests/test_integration.py index 870312e..7b60626 100644 --- a/midigen/tests/test_integration.py +++ b/midigen/tests/test_integration.py @@ -20,7 +20,7 @@ def test_complete_song_with_drums_chords_and_melody(self): ] for note in melody_notes: melody_track.add_note(note) - melody_track.add_note_off_messages() + # Track 1: Chord progression midi.add_track() @@ -35,7 +35,7 @@ def test_complete_song_with_drums_chords_and_melody(self): time_per_chord=480 ) chord_track.add_chord_progression(progression) - chord_track.add_note_off_messages() + # Track 2: Drums midi.add_track() @@ -49,7 +49,7 @@ def test_complete_song_with_drums_chords_and_melody(self): drum_kit.add_drum("Acoustic Snare", velocity=80, duration=240, time=1440) drum_track.add_drum_kit(drum_kit) - drum_track.add_note_off_messages() + # Verify tracks were created (includes default track 0) self.assertEqual(len(midi.tracks), 3) @@ -110,7 +110,7 @@ def test_overlapping_notes(self): track.add_note(note1) track.add_note(note2) track.add_note(note3) - track.add_note_off_messages() + # Verify all notes were added self.assertEqual(len(track.notes), 3) @@ -139,7 +139,7 @@ def test_complex_timing_scenarios(self): chord2 = Chord.create_major_triad(root2) track.add_chord(chord2) - track.add_note_off_messages() + # Verify timing structure self.assertEqual(len(track.notes), 7) # 3 from first chord + 1 note + 3 from second chord From ed50956aa7dcd50e60d8a4d660cc6226668a1278 Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:59:11 -0400 Subject: [PATCH 08/11] update examples and README for new architecture - example.py and song_example.py use current API - README: replace deprecated Legacy API section with architecture overview --- README.md | 20 +++++++------------- example.py | 5 +---- song_example.py | 11 +++++------ 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 3d162f1..4fb5535 100644 --- a/README.md +++ b/README.md @@ -279,22 +279,16 @@ track.add_chord(c_major_chord) midi_gen.save("low_level_example.mid") ``` -## Legacy API Support +## Architecture -For backward compatibility, the `Song` class still supports the older API pattern where `generate()` and `save()` are called directly on the song. This pattern is deprecated and will emit warnings: +The library is organized into four layers: -```python -# Legacy pattern (deprecated but still works) -from midigen import Song, Section, Key - -song = Song(key=Key("C", "major"), tempo=120) -song.add_section(Section("Verse", 8, "I-V-vi-IV")) -song.add_instrument("Acoustic Grand Piano") -song.generate(instrument_name="Acoustic Grand Piano") # Deprecated -song.save("my_song.mid") # Deprecated -``` +- **`midigen.theory`** — Pure music theory (Note, Key, Scale, roman numeral parsing, time conversion) +- **`midigen.composition`** — Musical structures (Chord, ChordProgression, Arpeggio, Melody, DrumKit, Section) +- **`midigen.protocol`** — MIDI protocol (Track, ChannelPool, instruments) +- **`midigen.api`** — High-level API (Song, MidiGen, MidiCompiler) -We recommend migrating to the `MidiCompiler` pattern shown in the examples above for better separation of concerns and more control over the compilation process. +All public names are available via `from midigen import ...`. ## Contributing diff --git a/example.py b/example.py index 1a01a3a..48ccee6 100644 --- a/example.py +++ b/example.py @@ -1,7 +1,4 @@ -from midigen.midigen import MidiGen -from midigen.note import Note -from midigen.chord import Chord -from midigen.key import Key, KEY_MAP +from midigen import MidiGen, Note, Chord, Key, KEY_MAP midi_gen = MidiGen(tempo=120, time_signature=(4, 4), key_signature=Key("C")) diff --git a/song_example.py b/song_example.py index 4ea2b45..fb27f02 100644 --- a/song_example.py +++ b/song_example.py @@ -1,4 +1,4 @@ -from midigen import Song, Section, Key +from midigen import Song, Section, Key, MidiCompiler # 1. Create a Song song = Song(key=Key("C", "major"), tempo=120) @@ -10,10 +10,9 @@ # 3. Add an instrument song.add_instrument("Acoustic Grand Piano") -# 4. Generate the song for the instrument -song.generate(instrument_name="Acoustic Grand Piano", octave=4, duration=480) - -# 5. Save the song -song.save("my_song.mid") +# 4. Compile and save +compiler = MidiCompiler(song) +compiler.compile() +compiler.save("my_song.mid") print("Song 'my_song.mid' created successfully.") From 8544957bc74d858f0713e2df50b5493d2aa36c61 Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:45:49 -0400 Subject: [PATCH 09/11] fix review findings: orphaned imports, missing exports, add tests - Remove unused imports (KEY_MAP/Key in chord.py, Optional in compiler.py and roman.py, Track/Drum in test_integration) - Add missing top-level exports: VALID_KEYS, GM1_DRUM_MAP, MAX_MIDI_TICKS, NOTE_ON, NOTE_OFF - Add Chord.build() tests (all types, intervals, invalid key) - Add Section validation tests (empty, double dash, invalid numeral) --- midigen/__init__.py | 8 +++--- midigen/api/compiler.py | 2 +- midigen/composition/chord.py | 1 - midigen/tests/test_chord.py | 43 ++++++++++++++++++++++++++++- midigen/tests/test_integration.py | 2 +- midigen/tests/test_section.py | 46 +++++++++++++++++++++++++++++++ midigen/theory/roman.py | 2 +- 7 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 midigen/tests/test_section.py diff --git a/midigen/__init__.py b/midigen/__init__.py index 373cb1a..7d019c1 100644 --- a/midigen/__init__.py +++ b/midigen/__init__.py @@ -1,5 +1,5 @@ -from .theory.note import Note -from .theory.key import Key, KEY_MAP +from .theory.note import Note, NOTE_ON, NOTE_OFF +from .theory.key import Key, KEY_MAP, VALID_KEYS from .theory.scale import Scale from .theory.time_utils import TimeConverter from .theory.roman import parse_roman_numeral, get_chord_pitches @@ -7,11 +7,11 @@ from .composition.chord import Chord, CHORD_TYPES from .composition.progression import ChordProgression from .composition.arpeggio import Arpeggio, ArpeggioPattern -from .composition.drums import DrumKit, Drum +from .composition.drums import DrumKit, Drum, GM1_DRUM_MAP from .composition.melody import Melody from .composition.section import Section -from .protocol.track import Track +from .protocol.track import Track, MAX_MIDI_TICKS from .protocol.channel_pool import ChannelPool, ChannelExhaustedError from .protocol.instruments import INSTRUMENT_MAP diff --git a/midigen/api/compiler.py b/midigen/api/compiler.py index e861caa..8347a8f 100644 --- a/midigen/api/compiler.py +++ b/midigen/api/compiler.py @@ -33,7 +33,7 @@ - Compiler truncates: I-IV (first 2 bars only) """ -from typing import Dict, List, Optional +from typing import Dict, List from midigen.api.midigen import MidiGen from midigen.api.song import Song diff --git a/midigen/composition/chord.py b/midigen/composition/chord.py index 30eb739..0590397 100644 --- a/midigen/composition/chord.py +++ b/midigen/composition/chord.py @@ -1,6 +1,5 @@ from typing import List from midigen.theory.note import Note -from midigen.theory.key import KEY_MAP, Key CHORD_TYPES = { diff --git a/midigen/tests/test_chord.py b/midigen/tests/test_chord.py index 3a23d93..43f64fb 100644 --- a/midigen/tests/test_chord.py +++ b/midigen/tests/test_chord.py @@ -1,6 +1,6 @@ from midigen.theory.note import Note from midigen.theory.key import KEY_MAP -from midigen.composition.chord import Chord +from midigen.composition.chord import Chord, CHORD_TYPES from midigen.composition.progression import ChordProgression from midigen.composition.arpeggio import Arpeggio, ArpeggioPattern import unittest @@ -309,3 +309,44 @@ def test_chord_timing(self): # This is tricky because the time is distributed among the notes. # The first note of the second chord should have the time of the first chord's duration. self.assertEqual(progression.chords[1].notes[0].time, 480) + + +class TestChordBuild(unittest.TestCase): + def setUp(self): + self.root = Note(KEY_MAP["C4"], 64, 480, 0) + + def test_build_all_chord_types(self): + """Every CHORD_TYPES entry produces a chord with correct note count.""" + for chord_type, intervals in CHORD_TYPES.items(): + chord = Chord.build(chord_type, self.root) + self.assertEqual( + len(chord.notes), len(intervals), + f"Chord.build('{chord_type}') produced {len(chord.notes)} notes, expected {len(intervals)}" + ) + + def test_build_intervals_match(self): + """Verify pitch offsets match the CHORD_TYPES intervals.""" + for chord_type, intervals in CHORD_TYPES.items(): + chord = Chord.build(chord_type, self.root) + root_pitch = self.root.pitch + actual_intervals = tuple(n.pitch - root_pitch for n in chord.notes) + self.assertEqual( + actual_intervals, intervals, + f"Chord.build('{chord_type}') intervals {actual_intervals} != {intervals}" + ) + + def test_build_invalid_type_raises(self): + """Invalid chord type name raises KeyError.""" + with self.assertRaises(KeyError): + Chord.build("nonexistent_chord", self.root) + + def test_build_matches_create_wrappers(self): + """Chord.build() produces same result as named create_* methods.""" + self.assertEqual( + Chord.create_major_triad(self.root).notes, + Chord.build("major_triad", self.root).notes + ) + self.assertEqual( + Chord.create_sus2(self.root).notes, + Chord.build("sus2", self.root).notes + ) diff --git a/midigen/tests/test_integration.py b/midigen/tests/test_integration.py index 7b60626..67ab309 100644 --- a/midigen/tests/test_integration.py +++ b/midigen/tests/test_integration.py @@ -1,5 +1,5 @@ import unittest -from midigen import MidiGen, Track, Note, Chord, ChordProgression, Key, DrumKit, Drum, Song, Section +from midigen import MidiGen, Note, Chord, ChordProgression, Key, DrumKit, Song, Section class TestIntegration(unittest.TestCase): diff --git a/midigen/tests/test_section.py b/midigen/tests/test_section.py new file mode 100644 index 0000000..4fcf549 --- /dev/null +++ b/midigen/tests/test_section.py @@ -0,0 +1,46 @@ +import unittest +from midigen.composition.section import Section + + +class TestSectionValidation(unittest.TestCase): + def test_valid_progressions(self): + """Valid Roman numeral progressions parse without error.""" + Section("Verse", 4, "I-V-vi-IV") + Section("Chorus", 8, "I") + Section("Bridge", 2, "ii-V") + Section("Intro", 4, "I-IV-V-I") + Section("Minor", 4, "i-iv-v-i") + Section("Seventh", 4, "V7-I") + Section("Diminished", 4, "vii°") + + def test_empty_string_raises(self): + with self.assertRaises(ValueError): + Section("Bad", 4, "") + + def test_whitespace_only_raises(self): + with self.assertRaises(ValueError): + Section("Bad", 4, " ") + + def test_double_dash_raises(self): + with self.assertRaises(ValueError): + Section("Bad", 4, "I--V") + + def test_trailing_dash_raises(self): + with self.assertRaises(ValueError): + Section("Bad", 4, "I-V-") + + def test_leading_dash_raises(self): + with self.assertRaises(ValueError): + Section("Bad", 4, "-I-V") + + def test_invalid_numeral_raises(self): + with self.assertRaises(ValueError): + Section("Bad", 4, "I-garbage-IV") + + def test_error_message_includes_progression(self): + try: + Section("Bad", 4, "I-xyz-IV") + self.fail("Should have raised ValueError") + except ValueError as e: + self.assertIn("I-xyz-IV", str(e)) + self.assertIn("xyz", str(e)) diff --git a/midigen/theory/roman.py b/midigen/theory/roman.py index 69a8007..d1b90fb 100644 --- a/midigen/theory/roman.py +++ b/midigen/theory/roman.py @@ -13,7 +13,7 @@ """ import re -from typing import List, Tuple, Optional +from typing import List from dataclasses import dataclass from enum import Enum From 3e209e26a73cebcbfcab2ddc882831fad816522b Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:08:28 -0400 Subject: [PATCH 10/11] remove remaining unused imports (Key in compiler, ParsedRomanNumeral in test) --- midigen/api/compiler.py | 1 - midigen/tests/test_roman.py | 1 - 2 files changed, 2 deletions(-) diff --git a/midigen/api/compiler.py b/midigen/api/compiler.py index 8347a8f..a7ebbd6 100644 --- a/midigen/api/compiler.py +++ b/midigen/api/compiler.py @@ -37,7 +37,6 @@ from midigen.api.midigen import MidiGen from midigen.api.song import Song -from midigen.theory.key import Key from midigen.theory.note import Note from midigen.composition.chord import Chord from midigen.composition.progression import ChordProgression diff --git a/midigen/tests/test_roman.py b/midigen/tests/test_roman.py index 7b7a639..3df6827 100644 --- a/midigen/tests/test_roman.py +++ b/midigen/tests/test_roman.py @@ -13,7 +13,6 @@ get_chord_pitches, get_note_names_for_pitches, ChordQuality, - ParsedRomanNumeral, MAJOR_SCALE_SEMITONES, MINOR_SCALE_SEMITONES, ) From 14c14ed410d661d70b3aac89017a23078dd66a7b Mon Sep 17 00:00:00 2001 From: Kyle Cain <22282320+cainky@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:24:12 -0400 Subject: [PATCH 11/11] switch CI from poetry to uv --- .github/workflows/tests.yml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a03a138..023470e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,23 +17,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - - name: Install Poetry - run: | - curl -sSL https://install.python-poetry.org | python3 - - echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Install uv + uses: astral-sh/setup-uv@v4 - - name: Configure Poetry - run: | - poetry config virtualenvs.in-project true + - name: Set up Python + run: uv python install 3.10 - name: Install dependencies - run: | - poetry install -vvv + run: uv sync - name: Run tests - run: poetry run python -m unittest discover + run: uv run pytest