songleaf 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- songleaf-0.0.2/.gitignore +120 -0
- songleaf-0.0.2/LICENSE +21 -0
- songleaf-0.0.2/PKG-INFO +119 -0
- songleaf-0.0.2/README.md +86 -0
- songleaf-0.0.2/pyproject.toml +192 -0
- songleaf-0.0.2/songleaf/__init__.py +43 -0
- songleaf-0.0.2/songleaf/__main__.py +55 -0
- songleaf-0.0.2/songleaf/model.py +154 -0
- songleaf-0.0.2/songleaf/parse.py +257 -0
- songleaf-0.0.2/songleaf/render.py +506 -0
- songleaf-0.0.2/songleaf/sources.py +261 -0
- songleaf-0.0.2/songleaf/store.py +97 -0
- songleaf-0.0.2/songleaf/tools.py +115 -0
- songleaf-0.0.2/tests/conftest.py +104 -0
- songleaf-0.0.2/tests/test_edge_cases.py +240 -0
- songleaf-0.0.2/tests/test_imports.py +16 -0
- songleaf-0.0.2/tests/test_model_and_store.py +74 -0
- songleaf-0.0.2/tests/test_parse.py +137 -0
- songleaf-0.0.2/tests/test_real_corpus.py +30 -0
- songleaf-0.0.2/tests/test_render.py +130 -0
- songleaf-0.0.2/tests/test_smoke.py +78 -0
- songleaf-0.0.2/tests/test_sources.py +82 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
.claude/handoffs/
|
|
2
|
+
.claude/scratch/
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
.DS_Store
|
|
11
|
+
# C extensions
|
|
12
|
+
*.so
|
|
13
|
+
|
|
14
|
+
# TLS certificates
|
|
15
|
+
## Ignore all PEM files anywhere
|
|
16
|
+
*.pem
|
|
17
|
+
## Also ignore any certs directory
|
|
18
|
+
certs/
|
|
19
|
+
|
|
20
|
+
# Distribution / packaging
|
|
21
|
+
.Python
|
|
22
|
+
build/
|
|
23
|
+
develop-eggs/
|
|
24
|
+
dist/
|
|
25
|
+
downloads/
|
|
26
|
+
eggs/
|
|
27
|
+
.eggs/
|
|
28
|
+
lib/
|
|
29
|
+
lib64/
|
|
30
|
+
parts/
|
|
31
|
+
sdist/
|
|
32
|
+
var/
|
|
33
|
+
wheels/
|
|
34
|
+
*.egg-info/
|
|
35
|
+
.installed.cfg
|
|
36
|
+
*.egg
|
|
37
|
+
MANIFEST
|
|
38
|
+
_build
|
|
39
|
+
|
|
40
|
+
# PyInstaller
|
|
41
|
+
# Usually these files are written by a python script from a template
|
|
42
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
43
|
+
*.manifest
|
|
44
|
+
*.spec
|
|
45
|
+
|
|
46
|
+
# Installer logs
|
|
47
|
+
pip-log.txt
|
|
48
|
+
pip-delete-this-directory.txt
|
|
49
|
+
|
|
50
|
+
# Unit test / coverage reports
|
|
51
|
+
htmlcov/
|
|
52
|
+
.tox/
|
|
53
|
+
.coverage
|
|
54
|
+
.coverage.*
|
|
55
|
+
.cache
|
|
56
|
+
nosetests.xml
|
|
57
|
+
coverage.xml
|
|
58
|
+
*.cover
|
|
59
|
+
.hypothesis/
|
|
60
|
+
.pytest_cache/
|
|
61
|
+
|
|
62
|
+
# Translations
|
|
63
|
+
*.mo
|
|
64
|
+
*.pot
|
|
65
|
+
|
|
66
|
+
# Django stuff:
|
|
67
|
+
*.log
|
|
68
|
+
local_settings.py
|
|
69
|
+
db.sqlite3
|
|
70
|
+
|
|
71
|
+
# Flask stuff:
|
|
72
|
+
instance/
|
|
73
|
+
.webassets-cache
|
|
74
|
+
|
|
75
|
+
# Scrapy stuff:
|
|
76
|
+
.scrapy
|
|
77
|
+
|
|
78
|
+
# Sphinx documentation
|
|
79
|
+
docs/_build/
|
|
80
|
+
docs/*
|
|
81
|
+
|
|
82
|
+
# PyBuilder
|
|
83
|
+
target/
|
|
84
|
+
|
|
85
|
+
# Jupyter Notebook
|
|
86
|
+
.ipynb_checkpoints
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
.python-version
|
|
90
|
+
|
|
91
|
+
# celery beat schedule file
|
|
92
|
+
celerybeat-schedule
|
|
93
|
+
|
|
94
|
+
# SageMath parsed files
|
|
95
|
+
*.sage.py
|
|
96
|
+
|
|
97
|
+
# Environments
|
|
98
|
+
.env
|
|
99
|
+
.venv
|
|
100
|
+
env/
|
|
101
|
+
venv/
|
|
102
|
+
ENV/
|
|
103
|
+
env.bak/
|
|
104
|
+
venv.bak/
|
|
105
|
+
|
|
106
|
+
# Spyder project settings
|
|
107
|
+
.spyderproject
|
|
108
|
+
.spyproject
|
|
109
|
+
|
|
110
|
+
# Rope project settings
|
|
111
|
+
.ropeproject
|
|
112
|
+
|
|
113
|
+
# mkdocs documentation
|
|
114
|
+
/site
|
|
115
|
+
|
|
116
|
+
# mypy
|
|
117
|
+
.mypy_cache/
|
|
118
|
+
|
|
119
|
+
# PyCharm
|
|
120
|
+
.idea
|
songleaf-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thor Whalen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
songleaf-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: songleaf
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Dense, readable one-page song sheets (lyrics + chords + score snippets) from searchable sources
|
|
5
|
+
Project-URL: Homepage, https://github.com/thorwhalen/songleaf
|
|
6
|
+
Project-URL: Repository, https://github.com/thorwhalen/songleaf
|
|
7
|
+
Project-URL: Documentation, https://thorwhalen.github.io/songleaf
|
|
8
|
+
Author: Thor Whalen
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: chords,lead-sheet,lyrics,music,pdf,song-sheet
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: cw<0.2,>=0.1.4
|
|
20
|
+
Requires-Dist: dol
|
|
21
|
+
Requires-Dist: pandas
|
|
22
|
+
Requires-Dist: rapidfuzz>=3
|
|
23
|
+
Requires-Dist: reportlab>=4
|
|
24
|
+
Requires-Dist: sung>=0.0.26
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
|
|
31
|
+
Requires-Dist: sphinx>=6.0; extra == 'docs'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# songleaf
|
|
35
|
+
|
|
36
|
+
Dense, readable one-page song sheets: lyrics and chords on one A4 page, with the lyrics as large as will fit.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install songleaf
|
|
40
|
+
python -m songleaf sheet "wonderwall oasis" # -> a one-page PDF in ~/.local/share/songleaf/sheets/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The default source is the Kaggle *chords-and-lyrics* corpus (about 135,000 songs), read from a local copy of its zip (see [Sources](#sources)).
|
|
44
|
+
|
|
45
|
+
## What the sheet looks like
|
|
46
|
+
|
|
47
|
+
- **One page, largest type that fits.** The lyric font size is found by search for each song. On 1,500 randomly sampled corpus songs, the median came out around 22 pt, and 1,498 of the 1,500 fit on one page. A song that cannot fit at the minimum size spills onto a second page.
|
|
48
|
+
- **Chords take no line of their own.** They are smaller and blue, start over the syllable they land on, and overlap the tops of the letters of their lyric line.
|
|
49
|
+
- **Lines share rows.** Consecutive lines of a paragraph are packed onto one row, separated by a light `/`. A blank line or a new section starts a new row.
|
|
50
|
+
- **Small labels.** Sections are grey prefixes (`V1`, `Ch`, `Br`); title, artist, capo and key are one small line at the top.
|
|
51
|
+
|
|
52
|
+
## Command line
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m songleaf search "wonderwall oasis" # fuzzy: typos and word order don't matter
|
|
56
|
+
python -m songleaf search --artist oasis --lyrics "roads winding"
|
|
57
|
+
python -m songleaf sheet "wonderwall oasis" --output wonderwall.pdf
|
|
58
|
+
python -m songleaf sheet "wonderwall oasis" --pick 2 # the second-best match
|
|
59
|
+
python -m songleaf songs # songs already stored
|
|
60
|
+
python -m songleaf sheet kaggle_chords:1234 # a song by its key: no search
|
|
61
|
+
python -m songleaf sheet kaggle_chords:1234 --refresh # fetch it again from its source
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The `songleaf` console script is the same command.
|
|
65
|
+
|
|
66
|
+
## Python
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import songleaf
|
|
70
|
+
|
|
71
|
+
hits = songleaf.search("wonderwall oasis")
|
|
72
|
+
song = songleaf.get_song(hits[0].key) # a Song: lyrics text + annotations
|
|
73
|
+
songleaf.render_dense_a4(song, "wonderwall.pdf") # {'font_size': ..., 'pages': 1, ...}
|
|
74
|
+
|
|
75
|
+
songleaf.sheet("wonderwall oasis") # search, store and render in one call
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## The song model
|
|
79
|
+
|
|
80
|
+
A `Song` is lyrics text plus *standoff* annotations anchored on character offsets, so annotation layers never touch the text:
|
|
81
|
+
|
|
82
|
+
- `section`: spans its lines, `{"label": "Chorus"}`;
|
|
83
|
+
- `chord`: a point on the first character of the syllable the chord lands on, `{"symbol": "G/B", "timing": "at"}` (`"before"` marks a chord that sounds ahead of that syllable);
|
|
84
|
+
- `score`: a link from a stretch of lyrics to a score snippet, `{"source", "id", "url", "format"}`.
|
|
85
|
+
|
|
86
|
+
Metadata (title, artist, capo, key) and provenance (source, id, url, licence) travel with the song, and `Song.to_dict()` is its JSON form.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from songleaf import Song, chord, section, parse_chords_over_lyrics
|
|
90
|
+
|
|
91
|
+
song = Song(
|
|
92
|
+
"Paper boats drift", [section(0, 17, "Verse 1"), chord(0, "G"), chord(6, "D")]
|
|
93
|
+
)
|
|
94
|
+
song = parse_chords_over_lyrics(
|
|
95
|
+
"G D\nPaper boats drift"
|
|
96
|
+
) # the same chords, from a chart
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Storage
|
|
100
|
+
|
|
101
|
+
Songs are kept in a `MutableMapping` (`songleaf.song_store()`): one JSON file per song under `~/.local/share/songleaf/songs/`. Rendered sheets go to `~/.local/share/songleaf/sheets/`. Set `SONGLEAF_DATA_DIR` to move both.
|
|
102
|
+
|
|
103
|
+
## Sources
|
|
104
|
+
|
|
105
|
+
`KaggleChordsSource` reads the [chords-and-lyrics dataset](https://www.kaggle.com/datasets/eitanbentora/chords-and-lyrics-dataset) through `sung`. It needs a local copy of the zip: point `SUNG_CHORDS_AND_LYRICS_ZIP` at it, or keep it where `haggle` downloads it (`$HAGGLE_ROOTDIR/zips/eitanbentora/chords-and-lyrics-dataset.zip`). Without a local copy, `sung` downloads it from Kaggle, which needs Kaggle credentials. The corpus was scraped from a chords site, so keep it to personal use.
|
|
106
|
+
|
|
107
|
+
Loading the corpus takes about five seconds on the first search in a process.
|
|
108
|
+
|
|
109
|
+
## Extending
|
|
110
|
+
|
|
111
|
+
Three keyword arguments are the extension points, on `sheet` (and `search` for sources):
|
|
112
|
+
|
|
113
|
+
- `sources=`: objects with a `name`, `search(query, *, title, artist, lyrics, limit)` returning `Hit`s, and `get(song_id)` returning a `Song`;
|
|
114
|
+
- `store=`: any `MutableMapping[str, Song]` (a `dict` works);
|
|
115
|
+
- `renderer=`: any `(song, output) -> dict` function.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
songleaf.sheet("paper boats", sources=[my_source], store={}, renderer=my_renderer)
|
|
119
|
+
```
|
songleaf-0.0.2/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# songleaf
|
|
2
|
+
|
|
3
|
+
Dense, readable one-page song sheets: lyrics and chords on one A4 page, with the lyrics as large as will fit.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install songleaf
|
|
7
|
+
python -m songleaf sheet "wonderwall oasis" # -> a one-page PDF in ~/.local/share/songleaf/sheets/
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The default source is the Kaggle *chords-and-lyrics* corpus (about 135,000 songs), read from a local copy of its zip (see [Sources](#sources)).
|
|
11
|
+
|
|
12
|
+
## What the sheet looks like
|
|
13
|
+
|
|
14
|
+
- **One page, largest type that fits.** The lyric font size is found by search for each song. On 1,500 randomly sampled corpus songs, the median came out around 22 pt, and 1,498 of the 1,500 fit on one page. A song that cannot fit at the minimum size spills onto a second page.
|
|
15
|
+
- **Chords take no line of their own.** They are smaller and blue, start over the syllable they land on, and overlap the tops of the letters of their lyric line.
|
|
16
|
+
- **Lines share rows.** Consecutive lines of a paragraph are packed onto one row, separated by a light `/`. A blank line or a new section starts a new row.
|
|
17
|
+
- **Small labels.** Sections are grey prefixes (`V1`, `Ch`, `Br`); title, artist, capo and key are one small line at the top.
|
|
18
|
+
|
|
19
|
+
## Command line
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m songleaf search "wonderwall oasis" # fuzzy: typos and word order don't matter
|
|
23
|
+
python -m songleaf search --artist oasis --lyrics "roads winding"
|
|
24
|
+
python -m songleaf sheet "wonderwall oasis" --output wonderwall.pdf
|
|
25
|
+
python -m songleaf sheet "wonderwall oasis" --pick 2 # the second-best match
|
|
26
|
+
python -m songleaf songs # songs already stored
|
|
27
|
+
python -m songleaf sheet kaggle_chords:1234 # a song by its key: no search
|
|
28
|
+
python -m songleaf sheet kaggle_chords:1234 --refresh # fetch it again from its source
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The `songleaf` console script is the same command.
|
|
32
|
+
|
|
33
|
+
## Python
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import songleaf
|
|
37
|
+
|
|
38
|
+
hits = songleaf.search("wonderwall oasis")
|
|
39
|
+
song = songleaf.get_song(hits[0].key) # a Song: lyrics text + annotations
|
|
40
|
+
songleaf.render_dense_a4(song, "wonderwall.pdf") # {'font_size': ..., 'pages': 1, ...}
|
|
41
|
+
|
|
42
|
+
songleaf.sheet("wonderwall oasis") # search, store and render in one call
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The song model
|
|
46
|
+
|
|
47
|
+
A `Song` is lyrics text plus *standoff* annotations anchored on character offsets, so annotation layers never touch the text:
|
|
48
|
+
|
|
49
|
+
- `section`: spans its lines, `{"label": "Chorus"}`;
|
|
50
|
+
- `chord`: a point on the first character of the syllable the chord lands on, `{"symbol": "G/B", "timing": "at"}` (`"before"` marks a chord that sounds ahead of that syllable);
|
|
51
|
+
- `score`: a link from a stretch of lyrics to a score snippet, `{"source", "id", "url", "format"}`.
|
|
52
|
+
|
|
53
|
+
Metadata (title, artist, capo, key) and provenance (source, id, url, licence) travel with the song, and `Song.to_dict()` is its JSON form.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from songleaf import Song, chord, section, parse_chords_over_lyrics
|
|
57
|
+
|
|
58
|
+
song = Song(
|
|
59
|
+
"Paper boats drift", [section(0, 17, "Verse 1"), chord(0, "G"), chord(6, "D")]
|
|
60
|
+
)
|
|
61
|
+
song = parse_chords_over_lyrics(
|
|
62
|
+
"G D\nPaper boats drift"
|
|
63
|
+
) # the same chords, from a chart
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Storage
|
|
67
|
+
|
|
68
|
+
Songs are kept in a `MutableMapping` (`songleaf.song_store()`): one JSON file per song under `~/.local/share/songleaf/songs/`. Rendered sheets go to `~/.local/share/songleaf/sheets/`. Set `SONGLEAF_DATA_DIR` to move both.
|
|
69
|
+
|
|
70
|
+
## Sources
|
|
71
|
+
|
|
72
|
+
`KaggleChordsSource` reads the [chords-and-lyrics dataset](https://www.kaggle.com/datasets/eitanbentora/chords-and-lyrics-dataset) through `sung`. It needs a local copy of the zip: point `SUNG_CHORDS_AND_LYRICS_ZIP` at it, or keep it where `haggle` downloads it (`$HAGGLE_ROOTDIR/zips/eitanbentora/chords-and-lyrics-dataset.zip`). Without a local copy, `sung` downloads it from Kaggle, which needs Kaggle credentials. The corpus was scraped from a chords site, so keep it to personal use.
|
|
73
|
+
|
|
74
|
+
Loading the corpus takes about five seconds on the first search in a process.
|
|
75
|
+
|
|
76
|
+
## Extending
|
|
77
|
+
|
|
78
|
+
Three keyword arguments are the extension points, on `sheet` (and `search` for sources):
|
|
79
|
+
|
|
80
|
+
- `sources=`: objects with a `name`, `search(query, *, title, artist, lyrics, limit)` returning `Hit`s, and `get(song_id)` returning a `Song`;
|
|
81
|
+
- `store=`: any `MutableMapping[str, Song]` (a `dict` works);
|
|
82
|
+
- `renderer=`: any `(song, output) -> dict` function.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
songleaf.sheet("paper boats", sources=[my_source], store={}, renderer=my_renderer)
|
|
86
|
+
```
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"hatchling",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "hatchling.build"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "songleaf"
|
|
9
|
+
version = "0.0.2"
|
|
10
|
+
description = "Dense, readable one-page song sheets (lyrics + chords + score snippets) from searchable sources"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
keywords = [
|
|
15
|
+
"music",
|
|
16
|
+
"lyrics",
|
|
17
|
+
"chords",
|
|
18
|
+
"song-sheet",
|
|
19
|
+
"lead-sheet",
|
|
20
|
+
"pdf",
|
|
21
|
+
]
|
|
22
|
+
authors = [
|
|
23
|
+
{ name = "Thor Whalen" },
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 3 - Alpha",
|
|
27
|
+
"Intended Audience :: End Users/Desktop",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"cw>=0.1.4,<0.2",
|
|
35
|
+
"dol",
|
|
36
|
+
"pandas",
|
|
37
|
+
"rapidfuzz>=3",
|
|
38
|
+
"reportlab>=4",
|
|
39
|
+
"sung>=0.0.26",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
songleaf = "songleaf.__main__:main"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/thorwhalen/songleaf"
|
|
47
|
+
Repository = "https://github.com/thorwhalen/songleaf"
|
|
48
|
+
Documentation = "https://thorwhalen.github.io/songleaf"
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=7.0",
|
|
53
|
+
"pytest-cov>=4.0",
|
|
54
|
+
"ruff>=0.1.0",
|
|
55
|
+
]
|
|
56
|
+
docs = [
|
|
57
|
+
"sphinx>=6.0",
|
|
58
|
+
"sphinx-rtd-theme>=1.0",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["songleaf"]
|
|
63
|
+
|
|
64
|
+
# An allowlist, so a source distribution never ships anything but code, tests and docs.
|
|
65
|
+
[tool.hatch.build.targets.sdist]
|
|
66
|
+
include = [
|
|
67
|
+
"songleaf/**/*.py",
|
|
68
|
+
"tests/**/*.py",
|
|
69
|
+
"README.md",
|
|
70
|
+
"LICENSE",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 88
|
|
75
|
+
target-version = "py310"
|
|
76
|
+
exclude = [
|
|
77
|
+
"**/*.ipynb",
|
|
78
|
+
".git",
|
|
79
|
+
".venv",
|
|
80
|
+
"build",
|
|
81
|
+
"dist",
|
|
82
|
+
"tests",
|
|
83
|
+
"examples",
|
|
84
|
+
"scrap",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint]
|
|
88
|
+
select = [
|
|
89
|
+
"D100",
|
|
90
|
+
]
|
|
91
|
+
ignore = [
|
|
92
|
+
"D203",
|
|
93
|
+
"E501",
|
|
94
|
+
"B905",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.pydocstyle]
|
|
98
|
+
convention = "google"
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.per-file-ignores]
|
|
101
|
+
"**/tests/*" = [
|
|
102
|
+
"D",
|
|
103
|
+
]
|
|
104
|
+
"**/examples/*" = [
|
|
105
|
+
"D",
|
|
106
|
+
]
|
|
107
|
+
"**/scrap/*" = [
|
|
108
|
+
"D",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[tool.pytest.ini_options]
|
|
112
|
+
minversion = "6.0"
|
|
113
|
+
testpaths = [
|
|
114
|
+
"tests",
|
|
115
|
+
]
|
|
116
|
+
doctest_optionflags = [
|
|
117
|
+
"NORMALIZE_WHITESPACE",
|
|
118
|
+
"ELLIPSIS",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
[tool.wads.ci]
|
|
122
|
+
project_name = ""
|
|
123
|
+
|
|
124
|
+
[tool.wads.ci.commands]
|
|
125
|
+
pre_test = []
|
|
126
|
+
test = []
|
|
127
|
+
post_test = []
|
|
128
|
+
lint = []
|
|
129
|
+
format = []
|
|
130
|
+
|
|
131
|
+
[tool.wads.ci.env]
|
|
132
|
+
required_envvars = []
|
|
133
|
+
test_envvars = []
|
|
134
|
+
extra_envvars = []
|
|
135
|
+
|
|
136
|
+
[tool.wads.ci.env.defaults]
|
|
137
|
+
|
|
138
|
+
[tool.wads.ci.quality.ruff]
|
|
139
|
+
enabled = true
|
|
140
|
+
|
|
141
|
+
[tool.wads.ci.quality.black]
|
|
142
|
+
enabled = false
|
|
143
|
+
|
|
144
|
+
[tool.wads.ci.quality.mypy]
|
|
145
|
+
enabled = false
|
|
146
|
+
|
|
147
|
+
[tool.wads.ci.testing]
|
|
148
|
+
enabled = true
|
|
149
|
+
python_versions = [
|
|
150
|
+
"3.10",
|
|
151
|
+
"3.12",
|
|
152
|
+
]
|
|
153
|
+
pytest_args = [
|
|
154
|
+
"-v",
|
|
155
|
+
"--tb=short",
|
|
156
|
+
]
|
|
157
|
+
coverage_enabled = true
|
|
158
|
+
coverage_threshold = 0
|
|
159
|
+
coverage_report_format = [
|
|
160
|
+
"term",
|
|
161
|
+
"xml",
|
|
162
|
+
]
|
|
163
|
+
exclude_paths = [
|
|
164
|
+
"examples",
|
|
165
|
+
"scrap",
|
|
166
|
+
]
|
|
167
|
+
test_on_windows = true
|
|
168
|
+
|
|
169
|
+
[tool.wads.ci.metrics]
|
|
170
|
+
enabled = true
|
|
171
|
+
config_path = ".github/umpyre-config.yml"
|
|
172
|
+
storage_branch = "code-metrics"
|
|
173
|
+
python_version = "3.10"
|
|
174
|
+
force_run = false
|
|
175
|
+
|
|
176
|
+
[tool.wads.ci.build]
|
|
177
|
+
sdist = true
|
|
178
|
+
wheel = true
|
|
179
|
+
|
|
180
|
+
[tool.wads.ci.publish]
|
|
181
|
+
enabled = true
|
|
182
|
+
skip_ci_marker = "[skip ci]"
|
|
183
|
+
publish_marker = "[publish]"
|
|
184
|
+
|
|
185
|
+
[tool.wads.ci.docs]
|
|
186
|
+
enabled = true
|
|
187
|
+
builder = "epythet"
|
|
188
|
+
ignore_paths = [
|
|
189
|
+
"tests/",
|
|
190
|
+
"scrap/",
|
|
191
|
+
"examples/",
|
|
192
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""songleaf: dense, readable one-page song sheets from searchable sources.
|
|
2
|
+
|
|
3
|
+
Find a song, keep it as lyrics plus anchored annotations (sections, chords,
|
|
4
|
+
provenance, score links), and render it onto one A4 page with the lyrics as
|
|
5
|
+
large as will fit.
|
|
6
|
+
|
|
7
|
+
>>> import songleaf
|
|
8
|
+
>>> song = songleaf.parse_chords_over_lyrics("C G\\nHello there my friend")
|
|
9
|
+
>>> [a.body["symbol"] for a in song.of_kind("chord")]
|
|
10
|
+
['C', 'G']
|
|
11
|
+
|
|
12
|
+
From a query to a PDF, on the Kaggle chords-and-lyrics corpus::
|
|
13
|
+
|
|
14
|
+
songleaf.sheet("wonderwall oasis") # -> {'path': '.../sheets/oasis-wonderwall.pdf', ...}
|
|
15
|
+
|
|
16
|
+
or ``python -m songleaf sheet "wonderwall oasis"``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from songleaf.model import Annotation, Song, chord, score_link, section
|
|
20
|
+
from songleaf.parse import parse_chords_over_lyrics
|
|
21
|
+
from songleaf.render import DenseStyle, fit_font_size, render_dense_a4
|
|
22
|
+
from songleaf.sources import Hit, KaggleChordsSource, get_song, search
|
|
23
|
+
from songleaf.store import song_store
|
|
24
|
+
from songleaf.tools import sheet, songs
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Annotation",
|
|
28
|
+
"DenseStyle",
|
|
29
|
+
"Hit",
|
|
30
|
+
"KaggleChordsSource",
|
|
31
|
+
"Song",
|
|
32
|
+
"chord",
|
|
33
|
+
"fit_font_size",
|
|
34
|
+
"get_song",
|
|
35
|
+
"parse_chords_over_lyrics",
|
|
36
|
+
"render_dense_a4",
|
|
37
|
+
"score_link",
|
|
38
|
+
"search",
|
|
39
|
+
"section",
|
|
40
|
+
"sheet",
|
|
41
|
+
"song_store",
|
|
42
|
+
"songs",
|
|
43
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
2
|
+
"""``python -m songleaf``: the command line, ``cw`` over :data:`songleaf.tools.TOOLS`.
|
|
3
|
+
|
|
4
|
+
Examples::
|
|
5
|
+
|
|
6
|
+
python -m songleaf search "wonderwall oasis"
|
|
7
|
+
python -m songleaf sheet "wonderwall oasis" # -> a one-page A4 PDF
|
|
8
|
+
python -m songleaf sheet kaggle_chords:1234 --output sheet.pdf
|
|
9
|
+
python -m songleaf songs
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import functools
|
|
13
|
+
import inspect
|
|
14
|
+
|
|
15
|
+
import cw
|
|
16
|
+
|
|
17
|
+
from songleaf import tools
|
|
18
|
+
|
|
19
|
+
#: Dependency-injection parameters: seams for library callers, not CLI options.
|
|
20
|
+
_SEAMS = ("sources", "store", "renderer")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _with_expected_errors(tool):
|
|
24
|
+
"""A failure the user can act on becomes one line on stderr, not a traceback."""
|
|
25
|
+
|
|
26
|
+
@functools.wraps(tool)
|
|
27
|
+
def command(*args, **kwargs):
|
|
28
|
+
try:
|
|
29
|
+
return tool(*args, **kwargs)
|
|
30
|
+
except (LookupError, RuntimeError, ValueError) as error:
|
|
31
|
+
raise cw.CommandError(str(error)) from error
|
|
32
|
+
|
|
33
|
+
return command
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv=None):
|
|
37
|
+
"""Run one songleaf command."""
|
|
38
|
+
config = {
|
|
39
|
+
tool.__name__: {
|
|
40
|
+
name: cw.HIDE
|
|
41
|
+
for name in _SEAMS
|
|
42
|
+
if name in inspect.signature(tool).parameters
|
|
43
|
+
}
|
|
44
|
+
for tool in tools.TOOLS
|
|
45
|
+
}
|
|
46
|
+
commands = [_with_expected_errors(tool) for tool in tools.TOOLS]
|
|
47
|
+
raise SystemExit(
|
|
48
|
+
cw.dispatch(
|
|
49
|
+
commands, argv, prog="songleaf", convention=cw.MODERN, config=config
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|