lexicon-python 0.3.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.
Files changed (45) hide show
  1. {lexicon_python-0.3.0/src/lexicon_python.egg-info → lexicon_python-0.4.0}/PKG-INFO +108 -49
  2. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/README.md +107 -48
  3. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/pyproject.toml +1 -1
  4. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/__init__.py +13 -1
  5. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/client.py +36 -0
  6. lexicon_python-0.4.0/src/lexicon/colors.py +29 -0
  7. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/tracks.py +226 -9
  8. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/tracks_types.py +60 -1
  9. lexicon_python-0.4.0/src/lexicon/tools/tempo.py +74 -0
  10. {lexicon_python-0.3.0 → lexicon_python-0.4.0/src/lexicon_python.egg-info}/PKG-INFO +108 -49
  11. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon_python.egg-info/SOURCES.txt +5 -0
  12. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_client.py +58 -15
  13. lexicon_python-0.4.0/tests/test_colors.py +34 -0
  14. lexicon_python-0.4.0/tests/test_integration_lifecycle.py +366 -0
  15. lexicon_python-0.4.0/tests/test_tools_tempo.py +79 -0
  16. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_tracks.py +263 -1
  17. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/LICENSE +0 -0
  18. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/setup.cfg +0 -0
  19. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/__init__.py +0 -0
  20. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/_common_types.py +0 -0
  21. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/base.py +0 -0
  22. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/playlist_tracks.py +0 -0
  23. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/playlist_tracks_types.py +0 -0
  24. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/playlists.py +0 -0
  25. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/playlists_types.py +0 -0
  26. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/tag_categories.py +0 -0
  27. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/tag_categories_types.py +0 -0
  28. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/tags.py +0 -0
  29. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/resources/tags_types.py +0 -0
  30. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/tools/__init__.py +0 -0
  31. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/tools/playlists.py +0 -0
  32. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/tools/tracks.py +0 -0
  33. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon/utils.py +0 -0
  34. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon_python.egg-info/dependency_links.txt +0 -0
  35. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon_python.egg-info/requires.txt +0 -0
  36. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/src/lexicon_python.egg-info/top_level.txt +0 -0
  37. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_base.py +0 -0
  38. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_common_types.py +0 -0
  39. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_playlist_tracks.py +0 -0
  40. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_playlists.py +0 -0
  41. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_playlists_types.py +0 -0
  42. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_tag_categories.py +0 -0
  43. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_tags.py +0 -0
  44. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_tools_playlists.py +0 -0
  45. {lexicon_python-0.3.0 → lexicon_python-0.4.0}/tests/test_tools_tracks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lexicon-python
3
- Version: 0.3.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
@@ -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
- optional validation. It is designed for scripting library automation, playlist
22
- management, and metadata edits while keeping a clean escape hatch to the raw API.
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
 
@@ -46,17 +46,22 @@ pip install lexicon-python
46
46
  ```python
47
47
  from lexicon import Lexicon
48
48
 
49
- lex = Lexicon()
49
+ lex = Lexicon() # verifies the API is reachable; raises LexiconConnectionError otherwise
50
+ playlist = lex.playlists.choose()
50
51
 
51
- # list tracks (default fields)
52
+ # list tracks (default fields: id, artist, title, bpm, key, ...)
52
53
  tracks = lex.tracks.list(limit=10) or []
53
54
  for t in tracks:
54
55
  print(t.get("artist"), "-", t.get("title"))
55
56
 
56
- # search
57
+ # search — pass fields=[...] for a narrow projection, or fields="all" for the
58
+ # full payload (tempomarkers, cuepoints, tags, etc.)
57
59
  results = lex.tracks.search({"artist": "Daft Punk"}) or []
58
60
  print("matches:", len(results))
59
61
 
62
+ # get a single track by id — always returns the full record
63
+ full = lex.tracks.get(123)
64
+
60
65
  # get a playlist by path
61
66
  playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
62
67
  print(playlist)
@@ -80,6 +85,8 @@ Other options:
80
85
  - `default_timeout`: request timeout in seconds
81
86
  - `session`: optional `requests.Session`
82
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).
83
90
 
84
91
  Environment variables:
85
92
 
@@ -92,8 +99,8 @@ export LEXICON_PORT=48624
92
99
 
93
100
  Many methods accept a `validation` parameter with three modes:
94
101
 
95
- - `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
96
- failures, but intended changes may be ignored.
102
+ - `"warn"` (default): invalid inputs are skipped with a warning. This avoids API failures, but intended changes may be
103
+ ignored.
97
104
  - `"strict"`: invalid inputs raise `ValueError`.
98
105
  - `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
99
106
 
@@ -107,9 +114,8 @@ lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
107
114
 
108
115
  ## Convenience vs Raw API
109
116
 
110
- This SDK adds several quality-of-life behaviors on top of the raw Lexicon API.
111
- In general, response shapes are unwrapped (e.g., `"data": {...}` is removed and
112
- 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).
113
119
 
114
120
  ### Additional Methods (Not in the Raw API)
115
121
 
@@ -122,28 +128,25 @@ single-item lists are collapsed to a single dict).
122
128
 
123
129
  ### Input Normalization (Broader Accepted Inputs)
124
130
 
125
- - **ID normalization**: Many methods accept lists of IDs instead of a single ID
126
- for easier batch operations.
127
- - **Field selection**: By default, `tracks.list()` and `tracks.search()` return a
128
- minimal set of fields rather than the full payload:
129
- - `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
130
- - 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.
131
136
  - **Search filter normalization**:
132
137
  - Text fields accept `None` (becomes `"NONE"` in filter context).
133
138
  - Numeric filters accept `None` (becomes `"0"`).
134
- - Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` /
135
- `datetime.datetime` inputs (time is stripped).
136
- - Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
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.
137
142
  - **Sort normalization**:
138
143
  - Accepts tuple shorthand: `[("title", "asc")]`.
139
144
  - **Track update helpers**:
140
- - Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
141
- name/number variants).
145
+ - Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts name/number variants).
142
146
  - Invalid entries can be skipped in `"warn"` mode without failing the update.
143
147
 
144
- If no SDK normalization is desired, use `validation="off"` and pass
145
- API-native payloads. API-native shapes are also accepted in `"warn"`/`"strict"`;
146
- 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,
147
150
  `lex.request(...)` can always be called directly.
148
151
 
149
152
  ## Tracks
@@ -159,23 +162,60 @@ tracks = lex.tracks.search({"artist": "Daft Punk"})
159
162
  added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
160
163
  updated = lex.tracks.update(123, {"title": "New Title"})
161
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)
162
171
  ```
163
172
 
164
173
  Notes:
165
174
 
166
175
  - `tracks.search()` results are capped at 1000 by the API.
167
176
  - `tracks.get_many()` preserves input order and returns `None` for missing IDs.
168
- - `fields=None` returns a minimal default set of fields.
169
- - In `validation="off"` mode, `fields=None` returns full payloads (API-default)
170
- - `fields="all"` or `fields="*"` requests full payloads.
171
- - `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.)
172
- may be populated later by Lexicon.
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
+ ```
173
214
 
174
215
  ### Track Search, Filters, and Sort
175
216
 
176
- `tracks.search(filter=...)` accepts a dict of field names and values. The SDK
177
- validates fields/values in `"warn"`/`"strict"` modes and can send API-native values
178
- 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.
179
219
 
180
220
  Examples:
181
221
 
@@ -233,6 +273,7 @@ Playlist type accepts:
233
273
  - `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
234
274
 
235
275
  ### Playlist tracks helpers
276
+
236
277
  For getting the tracks of a playlist or editing the tracklist.
237
278
 
238
279
  ```python
@@ -286,6 +327,7 @@ print(path)
286
327
  ```
287
328
 
288
329
  ## Raw Requests (Escape Hatch)
330
+
289
331
  The API can always be accessed directly with `lex.request`:
290
332
 
291
333
  ```python
@@ -304,8 +346,8 @@ High level namespaces (full mapping in `docs/resource-map.md`):
304
346
 
305
347
  ## Type Hints
306
348
 
307
- The SDK includes TypedDict and Literal types for payloads and enums. These are
308
- intended to improve editor autocomplete and static checks.
349
+ The SDK includes TypedDict and Literal types for payloads and enums. These are intended to improve editor autocomplete
350
+ and static checks.
309
351
 
310
352
  For full payload schemas and endpoint details, refer to the Lexicon API docs:
311
353
 
@@ -316,10 +358,10 @@ For full payload schemas and endpoint details, refer to the Lexicon API docs:
316
358
 
317
359
  ### Setup
318
360
 
319
- #### Pip
361
+ #### Pip
320
362
 
321
- Ensure that you have Python 3.9+ installed locally.
322
- To install all runtime and dev dependencies into a local virtual environment using pip:
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:
323
365
 
324
366
  ```bash
325
367
  python -m venv .venv
@@ -329,7 +371,8 @@ pip install -e ".[dev]"
329
371
 
330
372
  #### uv
331
373
 
332
- To use [uv](https://docs.astral.sh/uv/) to install all runtime and dev dependencies into a local virtual environment, simply install `uv` and run:
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:
333
376
 
334
377
  ```bash
335
378
  uv sync --dev
@@ -351,11 +394,10 @@ make run-integration-tests
351
394
 
352
395
  ### Linting and Formatting
353
396
 
354
- The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and
355
- formatting.
397
+ The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and formatting.
356
398
 
357
- `make test` runs the full suite: format, lint (with auto-fix), then tests.
358
- `make fix` runs all auto-fixers (lint + format) without running tests.
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.
359
401
 
360
402
  ```bash
361
403
  make test # format-fix → lint-fix → format-check → lint-check → tests
@@ -372,10 +414,28 @@ make format-check # check formatting
372
414
  make format-fix # auto-fix formatting
373
415
  ```
374
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
+
375
436
  ### CI
376
437
 
377
- GitHub Actions runs on every push to `main` and on pull requests
378
- targeting those branches. The pipeline includes:
438
+ GitHub Actions runs on every push to `main` and on pull requests targeting those branches. The pipeline includes:
379
439
 
380
440
  - **Tests** across Python 3.9, 3.10, 3.11, and 3.12
381
441
  - **Lint and format checks** via ruff on Python 3.12
@@ -384,9 +444,8 @@ Before opening a PR, make sure `make test` passes locally.
384
444
 
385
445
  ### Pull Requests
386
446
 
387
- A PR template is provided at `.github/pull_request_template.md`. When opening a
388
- PR, fill in the description, check the relevant change-type boxes, and confirm
389
- testing/checklist items.
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.
390
449
 
391
450
  ## License
392
451
 
@@ -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
- optional validation. It is designed for scripting library automation, playlist
7
- management, and metadata edits while keeping a clean escape hatch to the raw API.
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
 
@@ -31,17 +31,22 @@ pip install lexicon-python
31
31
  ```python
32
32
  from lexicon import Lexicon
33
33
 
34
- lex = Lexicon()
34
+ lex = Lexicon() # verifies the API is reachable; raises LexiconConnectionError otherwise
35
+ playlist = lex.playlists.choose()
35
36
 
36
- # list tracks (default fields)
37
+ # list tracks (default fields: id, artist, title, bpm, key, ...)
37
38
  tracks = lex.tracks.list(limit=10) or []
38
39
  for t in tracks:
39
40
  print(t.get("artist"), "-", t.get("title"))
40
41
 
41
- # search
42
+ # search — pass fields=[...] for a narrow projection, or fields="all" for the
43
+ # full payload (tempomarkers, cuepoints, tags, etc.)
42
44
  results = lex.tracks.search({"artist": "Daft Punk"}) or []
43
45
  print("matches:", len(results))
44
46
 
47
+ # get a single track by id — always returns the full record
48
+ full = lex.tracks.get(123)
49
+
45
50
  # get a playlist by path
46
51
  playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
47
52
  print(playlist)
@@ -65,6 +70,8 @@ Other options:
65
70
  - `default_timeout`: request timeout in seconds
66
71
  - `session`: optional `requests.Session`
67
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).
68
75
 
69
76
  Environment variables:
70
77
 
@@ -77,8 +84,8 @@ export LEXICON_PORT=48624
77
84
 
78
85
  Many methods accept a `validation` parameter with three modes:
79
86
 
80
- - `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
81
- failures, but intended changes may be ignored.
87
+ - `"warn"` (default): invalid inputs are skipped with a warning. This avoids API failures, but intended changes may be
88
+ ignored.
82
89
  - `"strict"`: invalid inputs raise `ValueError`.
83
90
  - `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
84
91
 
@@ -92,9 +99,8 @@ lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
92
99
 
93
100
  ## Convenience vs Raw API
94
101
 
95
- This SDK adds several quality-of-life behaviors on top of the raw Lexicon API.
96
- In general, response shapes are unwrapped (e.g., `"data": {...}` is removed and
97
- 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).
98
104
 
99
105
  ### Additional Methods (Not in the Raw API)
100
106
 
@@ -107,28 +113,25 @@ single-item lists are collapsed to a single dict).
107
113
 
108
114
  ### Input Normalization (Broader Accepted Inputs)
109
115
 
110
- - **ID normalization**: Many methods accept lists of IDs instead of a single ID
111
- for easier batch operations.
112
- - **Field selection**: By default, `tracks.list()` and `tracks.search()` return a
113
- minimal set of fields rather than the full payload:
114
- - `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
115
- - 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.
116
121
  - **Search filter normalization**:
117
122
  - Text fields accept `None` (becomes `"NONE"` in filter context).
118
123
  - Numeric filters accept `None` (becomes `"0"`).
119
- - Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` /
120
- `datetime.datetime` inputs (time is stripped).
121
- - Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
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.
122
127
  - **Sort normalization**:
123
128
  - Accepts tuple shorthand: `[("title", "asc")]`.
124
129
  - **Track update helpers**:
125
- - Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
126
- name/number variants).
130
+ - Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts name/number variants).
127
131
  - Invalid entries can be skipped in `"warn"` mode without failing the update.
128
132
 
129
- If no SDK normalization is desired, use `validation="off"` and pass
130
- API-native payloads. API-native shapes are also accepted in `"warn"`/`"strict"`;
131
- 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,
132
135
  `lex.request(...)` can always be called directly.
133
136
 
134
137
  ## Tracks
@@ -144,23 +147,60 @@ tracks = lex.tracks.search({"artist": "Daft Punk"})
144
147
  added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
145
148
  updated = lex.tracks.update(123, {"title": "New Title"})
146
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)
147
156
  ```
148
157
 
149
158
  Notes:
150
159
 
151
160
  - `tracks.search()` results are capped at 1000 by the API.
152
161
  - `tracks.get_many()` preserves input order and returns `None` for missing IDs.
153
- - `fields=None` returns a minimal default set of fields.
154
- - In `validation="off"` mode, `fields=None` returns full payloads (API-default)
155
- - `fields="all"` or `fields="*"` requests full payloads.
156
- - `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.)
157
- may be populated later by Lexicon.
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
+ ```
158
199
 
159
200
  ### Track Search, Filters, and Sort
160
201
 
161
- `tracks.search(filter=...)` accepts a dict of field names and values. The SDK
162
- validates fields/values in `"warn"`/`"strict"` modes and can send API-native values
163
- 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.
164
204
 
165
205
  Examples:
166
206
 
@@ -218,6 +258,7 @@ Playlist type accepts:
218
258
  - `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
219
259
 
220
260
  ### Playlist tracks helpers
261
+
221
262
  For getting the tracks of a playlist or editing the tracklist.
222
263
 
223
264
  ```python
@@ -271,6 +312,7 @@ print(path)
271
312
  ```
272
313
 
273
314
  ## Raw Requests (Escape Hatch)
315
+
274
316
  The API can always be accessed directly with `lex.request`:
275
317
 
276
318
  ```python
@@ -289,8 +331,8 @@ High level namespaces (full mapping in `docs/resource-map.md`):
289
331
 
290
332
  ## Type Hints
291
333
 
292
- The SDK includes TypedDict and Literal types for payloads and enums. These are
293
- intended to improve editor autocomplete and static checks.
334
+ The SDK includes TypedDict and Literal types for payloads and enums. These are intended to improve editor autocomplete
335
+ and static checks.
294
336
 
295
337
  For full payload schemas and endpoint details, refer to the Lexicon API docs:
296
338
 
@@ -301,10 +343,10 @@ For full payload schemas and endpoint details, refer to the Lexicon API docs:
301
343
 
302
344
  ### Setup
303
345
 
304
- #### Pip
346
+ #### Pip
305
347
 
306
- Ensure that you have Python 3.9+ installed locally.
307
- To install all runtime and dev dependencies into a local virtual environment using pip:
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:
308
350
 
309
351
  ```bash
310
352
  python -m venv .venv
@@ -314,7 +356,8 @@ pip install -e ".[dev]"
314
356
 
315
357
  #### uv
316
358
 
317
- To use [uv](https://docs.astral.sh/uv/) to install all runtime and dev dependencies into a local virtual environment, simply install `uv` and run:
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:
318
361
 
319
362
  ```bash
320
363
  uv sync --dev
@@ -336,11 +379,10 @@ make run-integration-tests
336
379
 
337
380
  ### Linting and Formatting
338
381
 
339
- The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and
340
- formatting.
382
+ The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and formatting.
341
383
 
342
- `make test` runs the full suite: format, lint (with auto-fix), then tests.
343
- `make fix` runs all auto-fixers (lint + format) without running tests.
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.
344
386
 
345
387
  ```bash
346
388
  make test # format-fix → lint-fix → format-check → lint-check → tests
@@ -357,10 +399,28 @@ make format-check # check formatting
357
399
  make format-fix # auto-fix formatting
358
400
  ```
359
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
+
360
421
  ### CI
361
422
 
362
- GitHub Actions runs on every push to `main` and on pull requests
363
- targeting those branches. The pipeline includes:
423
+ GitHub Actions runs on every push to `main` and on pull requests targeting those branches. The pipeline includes:
364
424
 
365
425
  - **Tests** across Python 3.9, 3.10, 3.11, and 3.12
366
426
  - **Lint and format checks** via ruff on Python 3.12
@@ -369,9 +429,8 @@ Before opening a PR, make sure `make test` passes locally.
369
429
 
370
430
  ### Pull Requests
371
431
 
372
- A PR template is provided at `.github/pull_request_template.md`. When opening a
373
- PR, fill in the description, check the relevant change-type boxes, and confirm
374
- testing/checklist items.
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.
375
434
 
376
435
  ## License
377
436
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lexicon-python"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Python client for the Lexicon DJ API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,23 +1,35 @@
1
1
  """Public package surface for the lexicon Python client."""
2
2
 
3
- from .client import DEFAULT_HOST, LEXICON_PORT, Lexicon
3
+ from .client import DEFAULT_HOST, LEXICON_PORT, Lexicon, LexiconConnectionError
4
+ from .colors import Color, color_rgb
4
5
  from .resources.playlists_types import PlaylistResponse
5
6
  from .resources.tag_categories_types import TagCategoryResponse
6
7
  from .resources.tags_types import TagResponse
7
8
  from .resources.tracks_types import (
8
9
  CuePointResponse,
10
+ CuePointUpdate,
11
+ TEMPO_RANGES,
9
12
  TempoMarkerResponse,
13
+ TempoMarkerUpdate,
14
+ TempoRange,
10
15
  TrackResponse,
11
16
  )
12
17
 
13
18
  __all__ = [
19
+ "Color",
20
+ "color_rgb",
14
21
  "DEFAULT_HOST",
15
22
  "LEXICON_PORT",
16
23
  "Lexicon",
24
+ "LexiconConnectionError",
17
25
  "TrackResponse",
18
26
  "PlaylistResponse",
19
27
  "TagResponse",
20
28
  "TagCategoryResponse",
21
29
  "CuePointResponse",
30
+ "CuePointUpdate",
22
31
  "TempoMarkerResponse",
32
+ "TempoMarkerUpdate",
33
+ "TempoRange",
34
+ "TEMPO_RANGES",
23
35
  ]