lexicon-python 0.2.0__tar.gz → 0.4.0__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.
- {lexicon_python-0.2.0/src/lexicon_python.egg-info → lexicon_python-0.4.0}/PKG-INFO +167 -44
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/README.md +165 -42
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/pyproject.toml +22 -2
- lexicon_python-0.4.0/src/lexicon/__init__.py +35 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/client.py +55 -4
- lexicon_python-0.4.0/src/lexicon/colors.py +29 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/_common_types.py +102 -53
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/base.py +3 -1
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/playlist_tracks.py +17 -13
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/playlists.py +121 -31
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/playlists_types.py +39 -19
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/tag_categories.py +28 -21
- lexicon_python-0.4.0/src/lexicon/resources/tag_categories_types.py +23 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/tags.py +10 -6
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/tags_types.py +12 -6
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/tracks.py +425 -53
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/tracks_types.py +421 -106
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/tools/__init__.py +0 -2
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/tools/playlists.py +5 -1
- lexicon_python-0.4.0/src/lexicon/tools/tempo.py +74 -0
- lexicon_python-0.4.0/src/lexicon/tools/tracks.py +130 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/utils.py +1 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0/src/lexicon_python.egg-info}/PKG-INFO +167 -44
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon_python.egg-info/SOURCES.txt +7 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_base.py +7 -2
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_client.py +61 -17
- lexicon_python-0.4.0/tests/test_colors.py +34 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_common_types.py +1 -1
- lexicon_python-0.4.0/tests/test_integration_lifecycle.py +366 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_playlist_tracks.py +25 -10
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_playlists.py +173 -36
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_playlists_types.py +4 -1
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_tag_categories.py +42 -19
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_tags.py +4 -1
- lexicon_python-0.4.0/tests/test_tools_tempo.py +79 -0
- lexicon_python-0.4.0/tests/test_tools_tracks.py +103 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_tracks.py +786 -118
- lexicon_python-0.2.0/src/lexicon/__init__.py +0 -8
- lexicon_python-0.2.0/src/lexicon/resources/tag_categories_types.py +0 -18
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/LICENSE +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/setup.cfg +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/__init__.py +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon/resources/playlist_tracks_types.py +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon_python.egg-info/dependency_links.txt +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon_python.egg-info/requires.txt +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/src/lexicon_python.egg-info/top_level.txt +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.4.0}/tests/test_tools_playlists.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lexicon-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Python client for the Lexicon DJ API
|
|
5
5
|
Author-email: Garrison Burger <burgerga123@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/photonicvelocity/lexicon-python
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: InquirerPy>=0.3
|
|
@@ -17,9 +17,9 @@ Dynamic: license-file
|
|
|
17
17
|
|
|
18
18
|
Python client for the Lexicon DJ Local API.
|
|
19
19
|
|
|
20
|
-
This SDK wraps the Lexicon Local API with resource groups, sensible defaults, and
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
This SDK wraps the Lexicon Local API with resource groups, sensible defaults, and optional validation. It is designed
|
|
21
|
+
for scripting library automation, playlist management, and metadata edits while keeping a clean escape hatch to the raw
|
|
22
|
+
API.
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
@@ -41,28 +41,27 @@ management, and metadata edits while keeping a clean escape hatch to the raw API
|
|
|
41
41
|
pip install lexicon-python
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Optional (for interactive playlist chooser):
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
pip install InquirerPy
|
|
48
|
-
```
|
|
49
|
-
|
|
50
44
|
## Quickstart
|
|
51
45
|
|
|
52
46
|
```python
|
|
53
47
|
from lexicon import Lexicon
|
|
54
48
|
|
|
55
|
-
lex = Lexicon()
|
|
49
|
+
lex = Lexicon() # verifies the API is reachable; raises LexiconConnectionError otherwise
|
|
50
|
+
playlist = lex.playlists.choose()
|
|
56
51
|
|
|
57
|
-
# list tracks (default fields)
|
|
52
|
+
# list tracks (default fields: id, artist, title, bpm, key, ...)
|
|
58
53
|
tracks = lex.tracks.list(limit=10) or []
|
|
59
54
|
for t in tracks:
|
|
60
55
|
print(t.get("artist"), "-", t.get("title"))
|
|
61
56
|
|
|
62
|
-
# search
|
|
57
|
+
# search — pass fields=[...] for a narrow projection, or fields="all" for the
|
|
58
|
+
# full payload (tempomarkers, cuepoints, tags, etc.)
|
|
63
59
|
results = lex.tracks.search({"artist": "Daft Punk"}) or []
|
|
64
60
|
print("matches:", len(results))
|
|
65
61
|
|
|
62
|
+
# get a single track by id — always returns the full record
|
|
63
|
+
full = lex.tracks.get(123)
|
|
64
|
+
|
|
66
65
|
# get a playlist by path
|
|
67
66
|
playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
|
|
68
67
|
print(playlist)
|
|
@@ -86,6 +85,8 @@ Other options:
|
|
|
86
85
|
- `default_timeout`: request timeout in seconds
|
|
87
86
|
- `session`: optional `requests.Session`
|
|
88
87
|
- `raise_on_error`: raise HTTP errors instead of returning None
|
|
88
|
+
- `verify_connection` (default `True`): probe the API on construction and raise `LexiconConnectionError` if Lexicon
|
|
89
|
+
isn't reachable. Set to `False` to build a client without a running Lexicon (e.g. for tests).
|
|
89
90
|
|
|
90
91
|
Environment variables:
|
|
91
92
|
|
|
@@ -98,8 +99,8 @@ export LEXICON_PORT=48624
|
|
|
98
99
|
|
|
99
100
|
Many methods accept a `validation` parameter with three modes:
|
|
100
101
|
|
|
101
|
-
- `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
|
|
102
|
-
|
|
102
|
+
- `"warn"` (default): invalid inputs are skipped with a warning. This avoids API failures, but intended changes may be
|
|
103
|
+
ignored.
|
|
103
104
|
- `"strict"`: invalid inputs raise `ValueError`.
|
|
104
105
|
- `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
|
|
105
106
|
|
|
@@ -113,9 +114,8 @@ lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
|
|
|
113
114
|
|
|
114
115
|
## Convenience vs Raw API
|
|
115
116
|
|
|
116
|
-
This SDK adds several quality-of-life behaviors on top of the raw Lexicon API.
|
|
117
|
-
|
|
118
|
-
single-item lists are collapsed to a single dict).
|
|
117
|
+
This SDK adds several quality-of-life behaviors on top of the raw Lexicon API. In general, response shapes are unwrapped
|
|
118
|
+
(e.g., `"data": {...}` is removed and single-item lists are collapsed to a single dict).
|
|
119
119
|
|
|
120
120
|
### Additional Methods (Not in the Raw API)
|
|
121
121
|
|
|
@@ -128,28 +128,25 @@ single-item lists are collapsed to a single dict).
|
|
|
128
128
|
|
|
129
129
|
### Input Normalization (Broader Accepted Inputs)
|
|
130
130
|
|
|
131
|
-
- **ID normalization**: Many methods accept lists of IDs instead of a single ID
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- Fields used as a search filter or sort item are also returned.
|
|
131
|
+
- **ID normalization**: Many methods accept lists of IDs instead of a single ID for easier batch operations.
|
|
132
|
+
- **Field selection**: By default, `tracks.list()` and `tracks.search()` return a minimal set of fields rather than the
|
|
133
|
+
full payload:
|
|
134
|
+
- `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
|
|
135
|
+
- Fields used as a search filter or sort item are also returned.
|
|
137
136
|
- **Search filter normalization**:
|
|
138
137
|
- Text fields accept `None` (becomes `"NONE"` in filter context).
|
|
139
138
|
- Numeric filters accept `None` (becomes `"0"`).
|
|
140
|
-
- Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` /
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
- Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` / `datetime.datetime` inputs (time is
|
|
140
|
+
stripped).
|
|
141
|
+
- Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
|
|
143
142
|
- **Sort normalization**:
|
|
144
143
|
- Accepts tuple shorthand: `[("title", "asc")]`.
|
|
145
144
|
- **Track update helpers**:
|
|
146
|
-
- Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
|
|
147
|
-
name/number variants).
|
|
145
|
+
- Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts name/number variants).
|
|
148
146
|
- Invalid entries can be skipped in `"warn"` mode without failing the update.
|
|
149
147
|
|
|
150
|
-
If no SDK normalization is desired, use `validation="off"` and pass
|
|
151
|
-
|
|
152
|
-
those modes simply add normalization/validation on top. For fully raw access,
|
|
148
|
+
If no SDK normalization is desired, use `validation="off"` and pass API-native payloads. API-native shapes are also
|
|
149
|
+
accepted in `"warn"`/`"strict"`; those modes simply add normalization/validation on top. For fully raw access,
|
|
153
150
|
`lex.request(...)` can always be called directly.
|
|
154
151
|
|
|
155
152
|
## Tracks
|
|
@@ -165,23 +162,60 @@ tracks = lex.tracks.search({"artist": "Daft Punk"})
|
|
|
165
162
|
added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
|
|
166
163
|
updated = lex.tracks.update(123, {"title": "New Title"})
|
|
167
164
|
lex.tracks.delete([123, 456])
|
|
165
|
+
|
|
166
|
+
# Replace tempomarkers while keeping cuepoints anchored to the same beats
|
|
167
|
+
lex.tracks.update_tempogrid(123, [{"startTime": 0.0, "bpm": 124.0}])
|
|
168
|
+
|
|
169
|
+
# Start Lexicon's analyzer on specific tracks
|
|
170
|
+
lex.tracks.analyze([123, 456], analyze_beatgrid=True, do_not_overwrite=False)
|
|
168
171
|
```
|
|
169
172
|
|
|
170
173
|
Notes:
|
|
171
174
|
|
|
172
175
|
- `tracks.search()` results are capped at 1000 by the API.
|
|
173
176
|
- `tracks.get_many()` preserves input order and returns `None` for missing IDs.
|
|
174
|
-
- `
|
|
175
|
-
|
|
176
|
-
- `fields=
|
|
177
|
-
- `
|
|
178
|
-
|
|
177
|
+
- `tracks.get()` always returns the full record (no `fields` parameter).
|
|
178
|
+
- For `tracks.list()` / `tracks.search()`:
|
|
179
|
+
- `fields=None` returns a minimal default set.
|
|
180
|
+
- In `validation="off"` mode, `fields=None` returns the full API payload.
|
|
181
|
+
- Pass a list of field names for a narrow projection (`fields=["id", "location"]`).
|
|
182
|
+
- `fields="all"` or `fields="*"` requests the full payload.
|
|
183
|
+
- Comma-separated strings (e.g. `"id,location"`) are _not_ accepted; pass a list. With `validation="warn"` the call
|
|
184
|
+
falls back to the default field set and logs.
|
|
185
|
+
- `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.) may be populated later by Lexicon.
|
|
186
|
+
- `tracks.update_tempogrid()` replaces tempomarkers while preserving each cuepoint's beat position by rewriting
|
|
187
|
+
`startTime`/`endTime` against the new grid. Use `tracks.update(track_id, {"tempomarkers": [...]})` directly if you
|
|
188
|
+
want the raw field replacement (cuepoints' seconds stay put, beats shift).
|
|
189
|
+
- `tracks.analyze()` returns as soon as Lexicon accepts the request. Analysis runs in the background and the API has no
|
|
190
|
+
completion signal, so poll `tracks.get()` and watch `tempomarkers`, `bpm`, `key`, or `dateModified`.
|
|
191
|
+
- `tracks.analyze(tempo_range=...)` takes one of the eight ids Lexicon's analyze dropdown stores: `"0_300"` (Any),
|
|
192
|
+
`"58_115"`, `"68_135"`, `"78_155"`, `"88_175"`, `"98_195"`, `"128_255"`, `"180_300"` (180+). An equivalent
|
|
193
|
+
`(min, max)` tuple works too. The value is written to Lexicon's stored analysis setting and persists after the call;
|
|
194
|
+
the boolean options apply to that run only.
|
|
195
|
+
|
|
196
|
+
### Track Payload Shapes
|
|
197
|
+
|
|
198
|
+
Rich-payload fields like `cuepoints` and `tempomarkers` aren't in the default field set; request them explicitly
|
|
199
|
+
(`fields=["cuepoints", "tempomarkers"]`) or ask for everything (`fields="all"`). Their shapes are typed via
|
|
200
|
+
`CuePointResponse` and `TempoMarkerResponse`, both re-exported from `lexicon`:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from lexicon import TrackResponse, TempoMarkerResponse, CuePointResponse
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Color values (cuepoint/tag/etc. `color` fields) are Lexicon-specific name strings declared by the `Color` literal type.
|
|
207
|
+
To convert a name to RGB:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from lexicon import color_rgb
|
|
211
|
+
|
|
212
|
+
color_rgb("red_dark") # → (158, 15, 7)
|
|
213
|
+
```
|
|
179
214
|
|
|
180
215
|
### Track Search, Filters, and Sort
|
|
181
216
|
|
|
182
|
-
`tracks.search(filter=...)` accepts a dict of field names and values. The SDK
|
|
183
|
-
|
|
184
|
-
in `"off"` mode.
|
|
217
|
+
`tracks.search(filter=...)` accepts a dict of field names and values. The SDK validates fields/values in
|
|
218
|
+
`"warn"`/`"strict"` modes and can send API-native values in `"off"` mode.
|
|
185
219
|
|
|
186
220
|
Examples:
|
|
187
221
|
|
|
@@ -239,6 +273,7 @@ Playlist type accepts:
|
|
|
239
273
|
- `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
|
|
240
274
|
|
|
241
275
|
### Playlist tracks helpers
|
|
276
|
+
|
|
242
277
|
For getting the tracks of a playlist or editing the tracklist.
|
|
243
278
|
|
|
244
279
|
```python
|
|
@@ -292,6 +327,7 @@ print(path)
|
|
|
292
327
|
```
|
|
293
328
|
|
|
294
329
|
## Raw Requests (Escape Hatch)
|
|
330
|
+
|
|
295
331
|
The API can always be accessed directly with `lex.request`:
|
|
296
332
|
|
|
297
333
|
```python
|
|
@@ -310,8 +346,8 @@ High level namespaces (full mapping in `docs/resource-map.md`):
|
|
|
310
346
|
|
|
311
347
|
## Type Hints
|
|
312
348
|
|
|
313
|
-
The SDK includes TypedDict and Literal types for payloads and enums. These are
|
|
314
|
-
|
|
349
|
+
The SDK includes TypedDict and Literal types for payloads and enums. These are intended to improve editor autocomplete
|
|
350
|
+
and static checks.
|
|
315
351
|
|
|
316
352
|
For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
317
353
|
|
|
@@ -320,10 +356,97 @@ For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
|
320
356
|
|
|
321
357
|
## Development
|
|
322
358
|
|
|
359
|
+
### Setup
|
|
360
|
+
|
|
361
|
+
#### Pip
|
|
362
|
+
|
|
363
|
+
Ensure that you have Python 3.9+ installed locally. To install all runtime and dev dependencies into a local virtual
|
|
364
|
+
environment using pip:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
python -m venv .venv
|
|
368
|
+
source .venv/bin/activate
|
|
369
|
+
pip install -e ".[dev]"
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### uv
|
|
373
|
+
|
|
374
|
+
To use [uv](https://docs.astral.sh/uv/) to install all runtime and dev dependencies into a local virtual environment,
|
|
375
|
+
simply install `uv` and run:
|
|
376
|
+
|
|
323
377
|
```bash
|
|
324
|
-
|
|
378
|
+
uv sync --dev
|
|
325
379
|
```
|
|
326
380
|
|
|
381
|
+
The lockfile (`uv.lock`) is checked in to ensure reproducible installs.
|
|
382
|
+
|
|
383
|
+
### Running Tests
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
# Unit tests
|
|
387
|
+
make run-tests
|
|
388
|
+
|
|
389
|
+
# Integration tests (requires Lexicon running)
|
|
390
|
+
# Note: Integration tests enforce an empty library state to avoid destructive edits on existing libraries.
|
|
391
|
+
# The fixture setup will back up the existing library, clear it for testing, and restore it afterward.
|
|
392
|
+
make run-integration-tests
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Linting and Formatting
|
|
396
|
+
|
|
397
|
+
The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and formatting.
|
|
398
|
+
|
|
399
|
+
`make test` runs the full suite: format, lint (with auto-fix), then tests. `make fix` runs all auto-fixers (lint +
|
|
400
|
+
format) without running tests.
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
make test # format-fix → lint-fix → format-check → lint-check → tests
|
|
404
|
+
make fix # lint-fix → format-fix
|
|
405
|
+
make clean # remove __pycache__, .pytest_cache, .ruff_cache, etc.
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
If you want to run the linters or formatters manually, you can use the following commands:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
make lint-check # check for lint issues
|
|
412
|
+
make lint-fix # auto-fix lint issues
|
|
413
|
+
make format-check # check formatting
|
|
414
|
+
make format-fix # auto-fix formatting
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Pre-commit hook (recommended)
|
|
418
|
+
|
|
419
|
+
The repo ships a [pre-commit](https://pre-commit.com/) config (`.pre-commit-config.yaml`) that mirrors the CI lint job.
|
|
420
|
+
After cloning, install the hook once:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
uv tool install pre-commit # or: pipx install pre-commit
|
|
424
|
+
pre-commit install
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`git commit` now runs `ruff format` + `ruff check --fix` on staged files and blocks the commit on any failure. Run
|
|
428
|
+
against the whole repo any time with:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
pre-commit run --all-files
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
The ruff version is pinned in `.pre-commit-config.yaml`; bump it when CI bumps to keep them aligned.
|
|
435
|
+
|
|
436
|
+
### CI
|
|
437
|
+
|
|
438
|
+
GitHub Actions runs on every push to `main` and on pull requests targeting those branches. The pipeline includes:
|
|
439
|
+
|
|
440
|
+
- **Tests** across Python 3.9, 3.10, 3.11, and 3.12
|
|
441
|
+
- **Lint and format checks** via ruff on Python 3.12
|
|
442
|
+
|
|
443
|
+
Before opening a PR, make sure `make test` passes locally.
|
|
444
|
+
|
|
445
|
+
### Pull Requests
|
|
446
|
+
|
|
447
|
+
A PR template is provided at `.github/pull_request_template.md`. When opening a PR, fill in the description, check the
|
|
448
|
+
relevant change-type boxes, and confirm testing/checklist items.
|
|
449
|
+
|
|
327
450
|
## License
|
|
328
451
|
|
|
329
452
|
MIT (see `LICENSE`).
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Python client for the Lexicon DJ Local API.
|
|
4
4
|
|
|
5
|
-
This SDK wraps the Lexicon Local API with resource groups, sensible defaults, and
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
This SDK wraps the Lexicon Local API with resource groups, sensible defaults, and optional validation. It is designed
|
|
6
|
+
for scripting library automation, playlist management, and metadata edits while keeping a clean escape hatch to the raw
|
|
7
|
+
API.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
@@ -26,28 +26,27 @@ management, and metadata edits while keeping a clean escape hatch to the raw API
|
|
|
26
26
|
pip install lexicon-python
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Optional (for interactive playlist chooser):
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
pip install InquirerPy
|
|
33
|
-
```
|
|
34
|
-
|
|
35
29
|
## Quickstart
|
|
36
30
|
|
|
37
31
|
```python
|
|
38
32
|
from lexicon import Lexicon
|
|
39
33
|
|
|
40
|
-
lex = Lexicon()
|
|
34
|
+
lex = Lexicon() # verifies the API is reachable; raises LexiconConnectionError otherwise
|
|
35
|
+
playlist = lex.playlists.choose()
|
|
41
36
|
|
|
42
|
-
# list tracks (default fields)
|
|
37
|
+
# list tracks (default fields: id, artist, title, bpm, key, ...)
|
|
43
38
|
tracks = lex.tracks.list(limit=10) or []
|
|
44
39
|
for t in tracks:
|
|
45
40
|
print(t.get("artist"), "-", t.get("title"))
|
|
46
41
|
|
|
47
|
-
# search
|
|
42
|
+
# search — pass fields=[...] for a narrow projection, or fields="all" for the
|
|
43
|
+
# full payload (tempomarkers, cuepoints, tags, etc.)
|
|
48
44
|
results = lex.tracks.search({"artist": "Daft Punk"}) or []
|
|
49
45
|
print("matches:", len(results))
|
|
50
46
|
|
|
47
|
+
# get a single track by id — always returns the full record
|
|
48
|
+
full = lex.tracks.get(123)
|
|
49
|
+
|
|
51
50
|
# get a playlist by path
|
|
52
51
|
playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
|
|
53
52
|
print(playlist)
|
|
@@ -71,6 +70,8 @@ Other options:
|
|
|
71
70
|
- `default_timeout`: request timeout in seconds
|
|
72
71
|
- `session`: optional `requests.Session`
|
|
73
72
|
- `raise_on_error`: raise HTTP errors instead of returning None
|
|
73
|
+
- `verify_connection` (default `True`): probe the API on construction and raise `LexiconConnectionError` if Lexicon
|
|
74
|
+
isn't reachable. Set to `False` to build a client without a running Lexicon (e.g. for tests).
|
|
74
75
|
|
|
75
76
|
Environment variables:
|
|
76
77
|
|
|
@@ -83,8 +84,8 @@ export LEXICON_PORT=48624
|
|
|
83
84
|
|
|
84
85
|
Many methods accept a `validation` parameter with three modes:
|
|
85
86
|
|
|
86
|
-
- `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
|
|
87
|
-
|
|
87
|
+
- `"warn"` (default): invalid inputs are skipped with a warning. This avoids API failures, but intended changes may be
|
|
88
|
+
ignored.
|
|
88
89
|
- `"strict"`: invalid inputs raise `ValueError`.
|
|
89
90
|
- `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
|
|
90
91
|
|
|
@@ -98,9 +99,8 @@ lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
|
|
|
98
99
|
|
|
99
100
|
## Convenience vs Raw API
|
|
100
101
|
|
|
101
|
-
This SDK adds several quality-of-life behaviors on top of the raw Lexicon API.
|
|
102
|
-
|
|
103
|
-
single-item lists are collapsed to a single dict).
|
|
102
|
+
This SDK adds several quality-of-life behaviors on top of the raw Lexicon API. In general, response shapes are unwrapped
|
|
103
|
+
(e.g., `"data": {...}` is removed and single-item lists are collapsed to a single dict).
|
|
104
104
|
|
|
105
105
|
### Additional Methods (Not in the Raw API)
|
|
106
106
|
|
|
@@ -113,28 +113,25 @@ single-item lists are collapsed to a single dict).
|
|
|
113
113
|
|
|
114
114
|
### Input Normalization (Broader Accepted Inputs)
|
|
115
115
|
|
|
116
|
-
- **ID normalization**: Many methods accept lists of IDs instead of a single ID
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
- Fields used as a search filter or sort item are also returned.
|
|
116
|
+
- **ID normalization**: Many methods accept lists of IDs instead of a single ID for easier batch operations.
|
|
117
|
+
- **Field selection**: By default, `tracks.list()` and `tracks.search()` return a minimal set of fields rather than the
|
|
118
|
+
full payload:
|
|
119
|
+
- `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
|
|
120
|
+
- Fields used as a search filter or sort item are also returned.
|
|
122
121
|
- **Search filter normalization**:
|
|
123
122
|
- Text fields accept `None` (becomes `"NONE"` in filter context).
|
|
124
123
|
- Numeric filters accept `None` (becomes `"0"`).
|
|
125
|
-
- Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` /
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
- Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` / `datetime.datetime` inputs (time is
|
|
125
|
+
stripped).
|
|
126
|
+
- Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
|
|
128
127
|
- **Sort normalization**:
|
|
129
128
|
- Accepts tuple shorthand: `[("title", "asc")]`.
|
|
130
129
|
- **Track update helpers**:
|
|
131
|
-
- Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
|
|
132
|
-
name/number variants).
|
|
130
|
+
- Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts name/number variants).
|
|
133
131
|
- Invalid entries can be skipped in `"warn"` mode without failing the update.
|
|
134
132
|
|
|
135
|
-
If no SDK normalization is desired, use `validation="off"` and pass
|
|
136
|
-
|
|
137
|
-
those modes simply add normalization/validation on top. For fully raw access,
|
|
133
|
+
If no SDK normalization is desired, use `validation="off"` and pass API-native payloads. API-native shapes are also
|
|
134
|
+
accepted in `"warn"`/`"strict"`; those modes simply add normalization/validation on top. For fully raw access,
|
|
138
135
|
`lex.request(...)` can always be called directly.
|
|
139
136
|
|
|
140
137
|
## Tracks
|
|
@@ -150,23 +147,60 @@ tracks = lex.tracks.search({"artist": "Daft Punk"})
|
|
|
150
147
|
added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
|
|
151
148
|
updated = lex.tracks.update(123, {"title": "New Title"})
|
|
152
149
|
lex.tracks.delete([123, 456])
|
|
150
|
+
|
|
151
|
+
# Replace tempomarkers while keeping cuepoints anchored to the same beats
|
|
152
|
+
lex.tracks.update_tempogrid(123, [{"startTime": 0.0, "bpm": 124.0}])
|
|
153
|
+
|
|
154
|
+
# Start Lexicon's analyzer on specific tracks
|
|
155
|
+
lex.tracks.analyze([123, 456], analyze_beatgrid=True, do_not_overwrite=False)
|
|
153
156
|
```
|
|
154
157
|
|
|
155
158
|
Notes:
|
|
156
159
|
|
|
157
160
|
- `tracks.search()` results are capped at 1000 by the API.
|
|
158
161
|
- `tracks.get_many()` preserves input order and returns `None` for missing IDs.
|
|
159
|
-
- `
|
|
160
|
-
|
|
161
|
-
- `fields=
|
|
162
|
-
- `
|
|
163
|
-
|
|
162
|
+
- `tracks.get()` always returns the full record (no `fields` parameter).
|
|
163
|
+
- For `tracks.list()` / `tracks.search()`:
|
|
164
|
+
- `fields=None` returns a minimal default set.
|
|
165
|
+
- In `validation="off"` mode, `fields=None` returns the full API payload.
|
|
166
|
+
- Pass a list of field names for a narrow projection (`fields=["id", "location"]`).
|
|
167
|
+
- `fields="all"` or `fields="*"` requests the full payload.
|
|
168
|
+
- Comma-separated strings (e.g. `"id,location"`) are _not_ accepted; pass a list. With `validation="warn"` the call
|
|
169
|
+
falls back to the default field set and logs.
|
|
170
|
+
- `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.) may be populated later by Lexicon.
|
|
171
|
+
- `tracks.update_tempogrid()` replaces tempomarkers while preserving each cuepoint's beat position by rewriting
|
|
172
|
+
`startTime`/`endTime` against the new grid. Use `tracks.update(track_id, {"tempomarkers": [...]})` directly if you
|
|
173
|
+
want the raw field replacement (cuepoints' seconds stay put, beats shift).
|
|
174
|
+
- `tracks.analyze()` returns as soon as Lexicon accepts the request. Analysis runs in the background and the API has no
|
|
175
|
+
completion signal, so poll `tracks.get()` and watch `tempomarkers`, `bpm`, `key`, or `dateModified`.
|
|
176
|
+
- `tracks.analyze(tempo_range=...)` takes one of the eight ids Lexicon's analyze dropdown stores: `"0_300"` (Any),
|
|
177
|
+
`"58_115"`, `"68_135"`, `"78_155"`, `"88_175"`, `"98_195"`, `"128_255"`, `"180_300"` (180+). An equivalent
|
|
178
|
+
`(min, max)` tuple works too. The value is written to Lexicon's stored analysis setting and persists after the call;
|
|
179
|
+
the boolean options apply to that run only.
|
|
180
|
+
|
|
181
|
+
### Track Payload Shapes
|
|
182
|
+
|
|
183
|
+
Rich-payload fields like `cuepoints` and `tempomarkers` aren't in the default field set; request them explicitly
|
|
184
|
+
(`fields=["cuepoints", "tempomarkers"]`) or ask for everything (`fields="all"`). Their shapes are typed via
|
|
185
|
+
`CuePointResponse` and `TempoMarkerResponse`, both re-exported from `lexicon`:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from lexicon import TrackResponse, TempoMarkerResponse, CuePointResponse
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Color values (cuepoint/tag/etc. `color` fields) are Lexicon-specific name strings declared by the `Color` literal type.
|
|
192
|
+
To convert a name to RGB:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from lexicon import color_rgb
|
|
196
|
+
|
|
197
|
+
color_rgb("red_dark") # → (158, 15, 7)
|
|
198
|
+
```
|
|
164
199
|
|
|
165
200
|
### Track Search, Filters, and Sort
|
|
166
201
|
|
|
167
|
-
`tracks.search(filter=...)` accepts a dict of field names and values. The SDK
|
|
168
|
-
|
|
169
|
-
in `"off"` mode.
|
|
202
|
+
`tracks.search(filter=...)` accepts a dict of field names and values. The SDK validates fields/values in
|
|
203
|
+
`"warn"`/`"strict"` modes and can send API-native values in `"off"` mode.
|
|
170
204
|
|
|
171
205
|
Examples:
|
|
172
206
|
|
|
@@ -224,6 +258,7 @@ Playlist type accepts:
|
|
|
224
258
|
- `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
|
|
225
259
|
|
|
226
260
|
### Playlist tracks helpers
|
|
261
|
+
|
|
227
262
|
For getting the tracks of a playlist or editing the tracklist.
|
|
228
263
|
|
|
229
264
|
```python
|
|
@@ -277,6 +312,7 @@ print(path)
|
|
|
277
312
|
```
|
|
278
313
|
|
|
279
314
|
## Raw Requests (Escape Hatch)
|
|
315
|
+
|
|
280
316
|
The API can always be accessed directly with `lex.request`:
|
|
281
317
|
|
|
282
318
|
```python
|
|
@@ -295,8 +331,8 @@ High level namespaces (full mapping in `docs/resource-map.md`):
|
|
|
295
331
|
|
|
296
332
|
## Type Hints
|
|
297
333
|
|
|
298
|
-
The SDK includes TypedDict and Literal types for payloads and enums. These are
|
|
299
|
-
|
|
334
|
+
The SDK includes TypedDict and Literal types for payloads and enums. These are intended to improve editor autocomplete
|
|
335
|
+
and static checks.
|
|
300
336
|
|
|
301
337
|
For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
302
338
|
|
|
@@ -305,10 +341,97 @@ For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
|
305
341
|
|
|
306
342
|
## Development
|
|
307
343
|
|
|
344
|
+
### Setup
|
|
345
|
+
|
|
346
|
+
#### Pip
|
|
347
|
+
|
|
348
|
+
Ensure that you have Python 3.9+ installed locally. To install all runtime and dev dependencies into a local virtual
|
|
349
|
+
environment using pip:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
python -m venv .venv
|
|
353
|
+
source .venv/bin/activate
|
|
354
|
+
pip install -e ".[dev]"
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### uv
|
|
358
|
+
|
|
359
|
+
To use [uv](https://docs.astral.sh/uv/) to install all runtime and dev dependencies into a local virtual environment,
|
|
360
|
+
simply install `uv` and run:
|
|
361
|
+
|
|
308
362
|
```bash
|
|
309
|
-
|
|
363
|
+
uv sync --dev
|
|
310
364
|
```
|
|
311
365
|
|
|
366
|
+
The lockfile (`uv.lock`) is checked in to ensure reproducible installs.
|
|
367
|
+
|
|
368
|
+
### Running Tests
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# Unit tests
|
|
372
|
+
make run-tests
|
|
373
|
+
|
|
374
|
+
# Integration tests (requires Lexicon running)
|
|
375
|
+
# Note: Integration tests enforce an empty library state to avoid destructive edits on existing libraries.
|
|
376
|
+
# The fixture setup will back up the existing library, clear it for testing, and restore it afterward.
|
|
377
|
+
make run-integration-tests
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Linting and Formatting
|
|
381
|
+
|
|
382
|
+
The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and formatting.
|
|
383
|
+
|
|
384
|
+
`make test` runs the full suite: format, lint (with auto-fix), then tests. `make fix` runs all auto-fixers (lint +
|
|
385
|
+
format) without running tests.
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
make test # format-fix → lint-fix → format-check → lint-check → tests
|
|
389
|
+
make fix # lint-fix → format-fix
|
|
390
|
+
make clean # remove __pycache__, .pytest_cache, .ruff_cache, etc.
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
If you want to run the linters or formatters manually, you can use the following commands:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
make lint-check # check for lint issues
|
|
397
|
+
make lint-fix # auto-fix lint issues
|
|
398
|
+
make format-check # check formatting
|
|
399
|
+
make format-fix # auto-fix formatting
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Pre-commit hook (recommended)
|
|
403
|
+
|
|
404
|
+
The repo ships a [pre-commit](https://pre-commit.com/) config (`.pre-commit-config.yaml`) that mirrors the CI lint job.
|
|
405
|
+
After cloning, install the hook once:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
uv tool install pre-commit # or: pipx install pre-commit
|
|
409
|
+
pre-commit install
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`git commit` now runs `ruff format` + `ruff check --fix` on staged files and blocks the commit on any failure. Run
|
|
413
|
+
against the whole repo any time with:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
pre-commit run --all-files
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The ruff version is pinned in `.pre-commit-config.yaml`; bump it when CI bumps to keep them aligned.
|
|
420
|
+
|
|
421
|
+
### CI
|
|
422
|
+
|
|
423
|
+
GitHub Actions runs on every push to `main` and on pull requests targeting those branches. The pipeline includes:
|
|
424
|
+
|
|
425
|
+
- **Tests** across Python 3.9, 3.10, 3.11, and 3.12
|
|
426
|
+
- **Lint and format checks** via ruff on Python 3.12
|
|
427
|
+
|
|
428
|
+
Before opening a PR, make sure `make test` passes locally.
|
|
429
|
+
|
|
430
|
+
### Pull Requests
|
|
431
|
+
|
|
432
|
+
A PR template is provided at `.github/pull_request_template.md`. When opening a PR, fill in the description, check the
|
|
433
|
+
relevant change-type boxes, and confirm testing/checklist items.
|
|
434
|
+
|
|
312
435
|
## License
|
|
313
436
|
|
|
314
437
|
MIT (see `LICENSE`).
|