lexicon-python 0.1.2__tar.gz → 0.3.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 (49) hide show
  1. lexicon_python-0.3.0/PKG-INFO +393 -0
  2. lexicon_python-0.3.0/README.md +378 -0
  3. lexicon_python-0.3.0/pyproject.toml +63 -0
  4. lexicon_python-0.3.0/src/lexicon/__init__.py +23 -0
  5. lexicon_python-0.3.0/src/lexicon/client.py +162 -0
  6. lexicon_python-0.3.0/src/lexicon/resources/__init__.py +15 -0
  7. lexicon_python-0.3.0/src/lexicon/resources/_common_types.py +228 -0
  8. lexicon_python-0.3.0/src/lexicon/resources/base.py +69 -0
  9. lexicon_python-0.3.0/src/lexicon/resources/playlist_tracks.py +302 -0
  10. lexicon_python-0.3.0/src/lexicon/resources/playlist_tracks_types.py +5 -0
  11. lexicon_python-0.3.0/src/lexicon/resources/playlists.py +601 -0
  12. lexicon_python-0.3.0/src/lexicon/resources/playlists_types.py +129 -0
  13. lexicon_python-0.3.0/src/lexicon/resources/tag_categories.py +253 -0
  14. lexicon_python-0.3.0/src/lexicon/resources/tag_categories_types.py +23 -0
  15. lexicon_python-0.3.0/src/lexicon/resources/tags.py +244 -0
  16. lexicon_python-0.3.0/src/lexicon/resources/tags_types.py +26 -0
  17. lexicon_python-0.3.0/src/lexicon/resources/tracks.py +765 -0
  18. lexicon_python-0.3.0/src/lexicon/resources/tracks_types.py +898 -0
  19. lexicon_python-0.3.0/src/lexicon/tools/__init__.py +1 -0
  20. lexicon_python-0.3.0/src/lexicon/tools/playlists.py +163 -0
  21. lexicon_python-0.3.0/src/lexicon/tools/tracks.py +130 -0
  22. lexicon_python-0.3.0/src/lexicon/utils.py +17 -0
  23. lexicon_python-0.3.0/src/lexicon_python.egg-info/PKG-INFO +393 -0
  24. lexicon_python-0.3.0/src/lexicon_python.egg-info/SOURCES.txt +38 -0
  25. lexicon_python-0.3.0/src/lexicon_python.egg-info/requires.txt +3 -0
  26. lexicon_python-0.3.0/tests/test_base.py +57 -0
  27. lexicon_python-0.3.0/tests/test_client.py +164 -0
  28. lexicon_python-0.3.0/tests/test_common_types.py +76 -0
  29. lexicon_python-0.3.0/tests/test_playlist_tracks.py +217 -0
  30. lexicon_python-0.3.0/tests/test_playlists.py +508 -0
  31. lexicon_python-0.3.0/tests/test_playlists_types.py +53 -0
  32. lexicon_python-0.3.0/tests/test_tag_categories.py +198 -0
  33. lexicon_python-0.3.0/tests/test_tags.py +166 -0
  34. lexicon_python-0.3.0/tests/test_tools_playlists.py +218 -0
  35. lexicon_python-0.3.0/tests/test_tools_tracks.py +103 -0
  36. lexicon_python-0.3.0/tests/test_tracks.py +1461 -0
  37. lexicon_python-0.1.2/PKG-INFO +0 -95
  38. lexicon_python-0.1.2/README.md +0 -81
  39. lexicon_python-0.1.2/pyproject.toml +0 -28
  40. lexicon_python-0.1.2/src/lexicon/__init__.py +0 -5
  41. lexicon_python-0.1.2/src/lexicon/lexicon.py +0 -766
  42. lexicon_python-0.1.2/src/lexicon_python.egg-info/PKG-INFO +0 -95
  43. lexicon_python-0.1.2/src/lexicon_python.egg-info/SOURCES.txt +0 -11
  44. lexicon_python-0.1.2/src/lexicon_python.egg-info/requires.txt +0 -2
  45. lexicon_python-0.1.2/tests/test_lexicon.py +0 -534
  46. {lexicon_python-0.1.2 → lexicon_python-0.3.0}/LICENSE +0 -0
  47. {lexicon_python-0.1.2 → lexicon_python-0.3.0}/setup.cfg +0 -0
  48. {lexicon_python-0.1.2 → lexicon_python-0.3.0}/src/lexicon_python.egg-info/dependency_links.txt +0 -0
  49. {lexicon_python-0.1.2 → lexicon_python-0.3.0}/src/lexicon_python.egg-info/top_level.txt +0 -0
@@ -0,0 +1,393 @@
1
+ Metadata-Version: 2.4
2
+ Name: lexicon-python
3
+ Version: 0.3.0
4
+ Summary: Python client for the Lexicon DJ API
5
+ Author-email: Garrison Burger <burgerga123@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/photonicvelocity/lexicon-python
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: InquirerPy>=0.3
12
+ Requires-Dist: requests>=2.31
13
+ Requires-Dist: typing_extensions>=4.0
14
+ Dynamic: license-file
15
+
16
+ # Lexicon Python
17
+
18
+ Python client for the Lexicon DJ Local API.
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.
23
+
24
+ ## Features
25
+
26
+ - Resource grouped client (`lex.tracks`, `lex.playlists`, `lex.tags`)
27
+ - Pagination handled for `tracks.list()` (API returns 1000 per page)
28
+ - Validation modes for inputs: warn (default), strict, or off
29
+ - Typed responses and payload hints (TypedDicts and Literals)
30
+ - Optional interactive playlist chooser via `InquirerPy`
31
+ - Raw request escape hatch via `lex.request(...)`
32
+
33
+ ## Requirements
34
+
35
+ - Python 3.9+
36
+ - Lexicon DJ running with Local API enabled
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install lexicon-python
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ ```python
47
+ from lexicon import Lexicon
48
+
49
+ lex = Lexicon()
50
+
51
+ # list tracks (default fields)
52
+ tracks = lex.tracks.list(limit=10) or []
53
+ for t in tracks:
54
+ print(t.get("artist"), "-", t.get("title"))
55
+
56
+ # search
57
+ results = lex.tracks.search({"artist": "Daft Punk"}) or []
58
+ print("matches:", len(results))
59
+
60
+ # get a playlist by path
61
+ playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
62
+ print(playlist)
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ By default the client targets:
68
+
69
+ - Host: `localhost`
70
+ - Port: `48624`
71
+
72
+ You can override these via constructor args or environment variables.
73
+
74
+ ```python
75
+ lex = Lexicon(host="127.0.0.1", port=48624, raise_on_error=False)
76
+ ```
77
+
78
+ Other options:
79
+
80
+ - `default_timeout`: request timeout in seconds
81
+ - `session`: optional `requests.Session`
82
+ - `raise_on_error`: raise HTTP errors instead of returning None
83
+
84
+ Environment variables:
85
+
86
+ ```bash
87
+ export LEXICON_HOST=localhost
88
+ export LEXICON_PORT=48624
89
+ ```
90
+
91
+ ## Validation Modes
92
+
93
+ Many methods accept a `validation` parameter with three modes:
94
+
95
+ - `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
96
+ failures, but intended changes may be ignored.
97
+ - `"strict"`: invalid inputs raise `ValueError`.
98
+ - `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
99
+
100
+ Example:
101
+
102
+ ```python
103
+ lex.tracks.search({"rating": "bad"}, validation="warn") # logs warning
104
+ lex.tracks.search({"rating": "bad"}, validation="strict") # raises
105
+ lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
106
+ ```
107
+
108
+ ## Convenience vs Raw API
109
+
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).
113
+
114
+ ### Additional Methods (Not in the Raw API)
115
+
116
+ - `tracks.get_many()` repeats `get()` and preserves input order.
117
+ - `playlists.get_many()` repeats `get()` and preserves input order.
118
+ - `playlists.tracks.get()` fetches track dicts in playlist order.
119
+ - `playlists.tracks.update()` replaces the full track list (remove + add).
120
+ - `playlists.choose()` provides an interactive chooser (wraps list + get).
121
+ - `playlists.get_path()` resolves a path from a playlist tree.
122
+
123
+ ### Input Normalization (Broader Accepted Inputs)
124
+
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
+ - **Search filter normalization**:
132
+ - Text fields accept `None` (becomes `"NONE"` in filter context).
133
+ - 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.
137
+ - **Sort normalization**:
138
+ - Accepts tuple shorthand: `[("title", "asc")]`.
139
+ - **Track update helpers**:
140
+ - Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
141
+ name/number variants).
142
+ - Invalid entries can be skipped in `"warn"` mode without failing the update.
143
+
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,
147
+ `lex.request(...)` can always be called directly.
148
+
149
+ ## Tracks
150
+
151
+ Common operations:
152
+
153
+ ```python
154
+ track = lex.tracks.get(123)
155
+ tracks = lex.tracks.get_many([1, 2, 3])
156
+ tracks = lex.tracks.list(limit=100)
157
+ tracks = lex.tracks.search({"artist": "Daft Punk"})
158
+
159
+ added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
160
+ updated = lex.tracks.update(123, {"title": "New Title"})
161
+ lex.tracks.delete([123, 456])
162
+ ```
163
+
164
+ Notes:
165
+
166
+ - `tracks.search()` results are capped at 1000 by the API.
167
+ - `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.
173
+
174
+ ### Track Search, Filters, and Sort
175
+
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.
179
+
180
+ Examples:
181
+
182
+ ```python
183
+ # text filters
184
+ lex.tracks.search({"artist": "Daft Punk"})
185
+
186
+ # numeric filters (strings)
187
+ lex.tracks.search({"bpm": "120"})
188
+ lex.tracks.search({"bpm": "120-128"})
189
+ lex.tracks.search({"bpm": ">=120"})
190
+
191
+ # date filters (YYYY-MM-DD)
192
+ lex.tracks.search({"dateAdded": "2024-01-01"})
193
+
194
+ # tag filters (comma-separated names)
195
+ # - default: OR across tags
196
+ # - prefix with "~" to require ALL tags (AND)
197
+ # - prefix with "!" to exclude a tag
198
+ lex.tracks.search({"tags": "Rock, Chill"}) # Rock OR Chill
199
+ lex.tracks.search({"tags": "~Rock, Chill"}) # Rock AND Chill
200
+ lex.tracks.search({"tags": "~Rock, !Chill"}) # Rock AND NOT Chill
201
+ ```
202
+
203
+ Tag filter details:
204
+
205
+ - Input is a single string with comma-separated tag names.
206
+ - Whitespace is ignored around commas.
207
+ - `~` at the start switches from OR to AND for the list.
208
+ - `!` before a tag name negates that tag.
209
+ - There is no supported way to search for “no tags”; `NONE` is not accepted.
210
+
211
+ Sort can be expressed in two shapes:
212
+
213
+ - API-native: list of dicts: `[{"field": "title", "dir": "asc"}]`
214
+ - Alternative: list of tuples: `[("title", "asc")]`
215
+
216
+ API-native dicts work in all modes; `validation="off"` requires the dict shape.
217
+
218
+ ## Playlists
219
+
220
+ ```python
221
+ playlist = lex.playlists.get(42)
222
+ playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="playlist")
223
+ tree = lex.playlists.list()
224
+
225
+ new_id = lex.playlists.add("Demo Playlist", playlist_type="playlist", parent_id=1)
226
+ lex.playlists.update(new_id, name="Renamed Playlist")
227
+ lex.playlists.delete([new_id])
228
+ ```
229
+
230
+ Playlist type accepts:
231
+
232
+ - `"folder"`, `"playlist"`, `"smartlist"`
233
+ - `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
234
+
235
+ ### Playlist tracks helpers
236
+ For getting the tracks of a playlist or editing the tracklist.
237
+
238
+ ```python
239
+ track_ids = lex.playlists.tracks.list(42)
240
+ tracks = lex.playlists.tracks.get(42)
241
+ lex.playlists.tracks.add(42, [1, 2, 3])
242
+ lex.playlists.tracks.remove(42, [1, 2])
243
+ lex.playlists.tracks.update(42, [3, 2, 1])
244
+ ```
245
+
246
+ ## Tags
247
+
248
+ ```python
249
+ tags = lex.tags.list()
250
+ new_tag = lex.tags.add(category_id=1, label="Demo Tag")
251
+ lex.tags.update(new_tag["id"], label="Renamed Tag")
252
+ lex.tags.delete(new_tag["id"])
253
+
254
+ categories = lex.tags.categories.list()
255
+ new_cat = lex.tags.categories.add(label="Demo Category", color="red")
256
+ lex.tags.categories.update(new_cat["id"], label="Renamed Category")
257
+ lex.tags.categories.delete(new_cat["id"])
258
+ ```
259
+
260
+ ## Tools
261
+
262
+ Interactive playlist chooser (requires InquirerPy):
263
+
264
+ ```python
265
+ choice = lex.playlists.choose()
266
+ print(choice)
267
+
268
+ # Or avoid an API call if you already have the tree
269
+ playlist_tree = lex.playlists.list()
270
+ choice = lex.tools.playlists.choose_playlist(playlist_tree) if playlist_tree else None
271
+ print(choice)
272
+ ```
273
+
274
+ Helper to resolve a playlist path from a tree:
275
+
276
+ ```python
277
+ path = lex.playlists.get_path(42)
278
+ print(path)
279
+
280
+ # Or avoid an API call if you already have the tree
281
+ playlist_tree = lex.playlists.list()
282
+ path = lex.tools.playlists.get_path_from_tree(playlist_tree, playlist_id=42)
283
+ print(path)
284
+
285
+ # -> ["Genres", "Drum & Bass"]
286
+ ```
287
+
288
+ ## Raw Requests (Escape Hatch)
289
+ The API can always be accessed directly with `lex.request`:
290
+
291
+ ```python
292
+ payload = lex.request("GET", "/tracks", params={"fields": "all"})
293
+ ```
294
+
295
+ ## Resource Overview
296
+
297
+ High level namespaces (full mapping in `docs/resource-map.md`):
298
+
299
+ - `lex.tracks`: get, get_many, list, search, add, update, delete
300
+ - `lex.playlists`: get, get_many, list (tree root), get_path, get_by_path, add, update, delete, choose
301
+ - `lex.playlists.tracks`: list (IDs), get (track dicts), add, remove, update
302
+ - `lex.tags`: list, add, update, delete
303
+ - `lex.tags.categories`: list, add, update, delete
304
+
305
+ ## Type Hints
306
+
307
+ The SDK includes TypedDict and Literal types for payloads and enums. These are
308
+ intended to improve editor autocomplete and static checks.
309
+
310
+ For full payload schemas and endpoint details, refer to the Lexicon API docs:
311
+
312
+ - https://www.lexicondj.com/docs/developers/api
313
+ - https://www.lexicondj.com/developer/api-docs.yaml
314
+
315
+ ## Development
316
+
317
+ ### Setup
318
+
319
+ #### Pip
320
+
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:
323
+
324
+ ```bash
325
+ python -m venv .venv
326
+ source .venv/bin/activate
327
+ pip install -e ".[dev]"
328
+ ```
329
+
330
+ #### uv
331
+
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:
333
+
334
+ ```bash
335
+ uv sync --dev
336
+ ```
337
+
338
+ The lockfile (`uv.lock`) is checked in to ensure reproducible installs.
339
+
340
+ ### Running Tests
341
+
342
+ ```bash
343
+ # Unit tests
344
+ make run-tests
345
+
346
+ # Integration tests (requires Lexicon running)
347
+ # Note: Integration tests enforce an empty library state to avoid destructive edits on existing libraries.
348
+ # The fixture setup will back up the existing library, clear it for testing, and restore it afterward.
349
+ make run-integration-tests
350
+ ```
351
+
352
+ ### Linting and Formatting
353
+
354
+ The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and
355
+ formatting.
356
+
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.
359
+
360
+ ```bash
361
+ make test # format-fix → lint-fix → format-check → lint-check → tests
362
+ make fix # lint-fix → format-fix
363
+ make clean # remove __pycache__, .pytest_cache, .ruff_cache, etc.
364
+ ```
365
+
366
+ If you want to run the linters or formatters manually, you can use the following commands:
367
+
368
+ ```bash
369
+ make lint-check # check for lint issues
370
+ make lint-fix # auto-fix lint issues
371
+ make format-check # check formatting
372
+ make format-fix # auto-fix formatting
373
+ ```
374
+
375
+ ### CI
376
+
377
+ GitHub Actions runs on every push to `main` and on pull requests
378
+ targeting those branches. The pipeline includes:
379
+
380
+ - **Tests** across Python 3.9, 3.10, 3.11, and 3.12
381
+ - **Lint and format checks** via ruff on Python 3.12
382
+
383
+ Before opening a PR, make sure `make test` passes locally.
384
+
385
+ ### Pull Requests
386
+
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.
390
+
391
+ ## License
392
+
393
+ MIT (see `LICENSE`).