lexicon-python 0.1.1__tar.gz → 0.2.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 (46) hide show
  1. lexicon_python-0.2.0/PKG-INFO +329 -0
  2. lexicon_python-0.2.0/README.md +314 -0
  3. {lexicon_python-0.1.1 → lexicon_python-0.2.0}/pyproject.toml +17 -2
  4. lexicon_python-0.2.0/src/lexicon/__init__.py +8 -0
  5. lexicon_python-0.2.0/src/lexicon/client.py +147 -0
  6. lexicon_python-0.2.0/src/lexicon/resources/__init__.py +15 -0
  7. lexicon_python-0.2.0/src/lexicon/resources/_common_types.py +179 -0
  8. lexicon_python-0.2.0/src/lexicon/resources/base.py +67 -0
  9. lexicon_python-0.2.0/src/lexicon/resources/playlist_tracks.py +298 -0
  10. lexicon_python-0.2.0/src/lexicon/resources/playlist_tracks_types.py +5 -0
  11. lexicon_python-0.2.0/src/lexicon/resources/playlists.py +511 -0
  12. lexicon_python-0.2.0/src/lexicon/resources/playlists_types.py +109 -0
  13. lexicon_python-0.2.0/src/lexicon/resources/tag_categories.py +246 -0
  14. lexicon_python-0.2.0/src/lexicon/resources/tag_categories_types.py +18 -0
  15. lexicon_python-0.2.0/src/lexicon/resources/tags.py +240 -0
  16. lexicon_python-0.2.0/src/lexicon/resources/tags_types.py +20 -0
  17. lexicon_python-0.2.0/src/lexicon/resources/tracks.py +610 -0
  18. lexicon_python-0.2.0/src/lexicon/resources/tracks_types.py +642 -0
  19. lexicon_python-0.2.0/src/lexicon/tools/__init__.py +3 -0
  20. lexicon_python-0.2.0/src/lexicon/tools/playlists.py +159 -0
  21. lexicon_python-0.2.0/src/lexicon/utils.py +16 -0
  22. lexicon_python-0.2.0/src/lexicon_python.egg-info/PKG-INFO +329 -0
  23. lexicon_python-0.2.0/src/lexicon_python.egg-info/SOURCES.txt +36 -0
  24. lexicon_python-0.2.0/src/lexicon_python.egg-info/requires.txt +3 -0
  25. lexicon_python-0.2.0/tests/test_base.py +52 -0
  26. lexicon_python-0.2.0/tests/test_client.py +163 -0
  27. lexicon_python-0.2.0/tests/test_common_types.py +76 -0
  28. lexicon_python-0.2.0/tests/test_playlist_tracks.py +202 -0
  29. lexicon_python-0.2.0/tests/test_playlists.py +371 -0
  30. lexicon_python-0.2.0/tests/test_playlists_types.py +50 -0
  31. lexicon_python-0.2.0/tests/test_tag_categories.py +175 -0
  32. lexicon_python-0.2.0/tests/test_tags.py +163 -0
  33. lexicon_python-0.2.0/tests/test_tools_playlists.py +218 -0
  34. lexicon_python-0.2.0/tests/test_tracks.py +1055 -0
  35. lexicon_python-0.1.1/PKG-INFO +0 -95
  36. lexicon_python-0.1.1/README.md +0 -81
  37. lexicon_python-0.1.1/src/lexicon/__init__.py +0 -5
  38. lexicon_python-0.1.1/src/lexicon/lexicon.py +0 -697
  39. lexicon_python-0.1.1/src/lexicon_python.egg-info/PKG-INFO +0 -95
  40. lexicon_python-0.1.1/src/lexicon_python.egg-info/SOURCES.txt +0 -11
  41. lexicon_python-0.1.1/src/lexicon_python.egg-info/requires.txt +0 -2
  42. lexicon_python-0.1.1/tests/test_lexicon.py +0 -509
  43. {lexicon_python-0.1.1 → lexicon_python-0.2.0}/LICENSE +0 -0
  44. {lexicon_python-0.1.1 → lexicon_python-0.2.0}/setup.cfg +0 -0
  45. {lexicon_python-0.1.1 → lexicon_python-0.2.0}/src/lexicon_python.egg-info/dependency_links.txt +0 -0
  46. {lexicon_python-0.1.1 → lexicon_python-0.2.0}/src/lexicon_python.egg-info/top_level.txt +0 -0
@@ -0,0 +1,329 @@
1
+ Metadata-Version: 2.4
2
+ Name: lexicon-python
3
+ Version: 0.2.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.9
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
+ Optional (for interactive playlist chooser):
45
+
46
+ ```bash
47
+ pip install InquirerPy
48
+ ```
49
+
50
+ ## Quickstart
51
+
52
+ ```python
53
+ from lexicon import Lexicon
54
+
55
+ lex = Lexicon()
56
+
57
+ # list tracks (default fields)
58
+ tracks = lex.tracks.list(limit=10) or []
59
+ for t in tracks:
60
+ print(t.get("artist"), "-", t.get("title"))
61
+
62
+ # search
63
+ results = lex.tracks.search({"artist": "Daft Punk"}) or []
64
+ print("matches:", len(results))
65
+
66
+ # get a playlist by path
67
+ playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
68
+ print(playlist)
69
+ ```
70
+
71
+ ## Configuration
72
+
73
+ By default the client targets:
74
+
75
+ - Host: `localhost`
76
+ - Port: `48624`
77
+
78
+ You can override these via constructor args or environment variables.
79
+
80
+ ```python
81
+ lex = Lexicon(host="127.0.0.1", port=48624, raise_on_error=False)
82
+ ```
83
+
84
+ Other options:
85
+
86
+ - `default_timeout`: request timeout in seconds
87
+ - `session`: optional `requests.Session`
88
+ - `raise_on_error`: raise HTTP errors instead of returning None
89
+
90
+ Environment variables:
91
+
92
+ ```bash
93
+ export LEXICON_HOST=localhost
94
+ export LEXICON_PORT=48624
95
+ ```
96
+
97
+ ## Validation Modes
98
+
99
+ Many methods accept a `validation` parameter with three modes:
100
+
101
+ - `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
102
+ failures, but intended changes may be ignored.
103
+ - `"strict"`: invalid inputs raise `ValueError`.
104
+ - `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
105
+
106
+ Example:
107
+
108
+ ```python
109
+ lex.tracks.search({"rating": "bad"}, validation="warn") # logs warning
110
+ lex.tracks.search({"rating": "bad"}, validation="strict") # raises
111
+ lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
112
+ ```
113
+
114
+ ## Convenience vs Raw API
115
+
116
+ This SDK adds several quality-of-life behaviors on top of the raw Lexicon API.
117
+ In general, response shapes are unwrapped (e.g., `"data": {...}` is removed and
118
+ single-item lists are collapsed to a single dict).
119
+
120
+ ### Additional Methods (Not in the Raw API)
121
+
122
+ - `tracks.get_many()` repeats `get()` and preserves input order.
123
+ - `playlists.get_many()` repeats `get()` and preserves input order.
124
+ - `playlists.tracks.get()` fetches track dicts in playlist order.
125
+ - `playlists.tracks.update()` replaces the full track list (remove + add).
126
+ - `playlists.choose()` provides an interactive chooser (wraps list + get).
127
+ - `playlists.get_path()` resolves a path from a playlist tree.
128
+
129
+ ### Input Normalization (Broader Accepted Inputs)
130
+
131
+ - **ID normalization**: Many methods accept lists of IDs instead of a single ID
132
+ for easier batch operations.
133
+ - **Field selection**: By default, `tracks.list()` and `tracks.search()` return a
134
+ minimal set of fields rather than the full payload:
135
+ - `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
136
+ - Fields used as a search filter or sort item are also returned.
137
+ - **Search filter normalization**:
138
+ - Text fields accept `None` (becomes `"NONE"` in filter context).
139
+ - Numeric filters accept `None` (becomes `"0"`).
140
+ - Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` /
141
+ `datetime.datetime` inputs (time is stripped).
142
+ - Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
143
+ - **Sort normalization**:
144
+ - Accepts tuple shorthand: `[("title", "asc")]`.
145
+ - **Track update helpers**:
146
+ - Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
147
+ name/number variants).
148
+ - Invalid entries can be skipped in `"warn"` mode without failing the update.
149
+
150
+ If no SDK normalization is desired, use `validation="off"` and pass
151
+ API-native payloads. API-native shapes are also accepted in `"warn"`/`"strict"`;
152
+ those modes simply add normalization/validation on top. For fully raw access,
153
+ `lex.request(...)` can always be called directly.
154
+
155
+ ## Tracks
156
+
157
+ Common operations:
158
+
159
+ ```python
160
+ track = lex.tracks.get(123)
161
+ tracks = lex.tracks.get_many([1, 2, 3])
162
+ tracks = lex.tracks.list(limit=100)
163
+ tracks = lex.tracks.search({"artist": "Daft Punk"})
164
+
165
+ added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
166
+ updated = lex.tracks.update(123, {"title": "New Title"})
167
+ lex.tracks.delete([123, 456])
168
+ ```
169
+
170
+ Notes:
171
+
172
+ - `tracks.search()` results are capped at 1000 by the API.
173
+ - `tracks.get_many()` preserves input order and returns `None` for missing IDs.
174
+ - `fields=None` returns a minimal default set of fields.
175
+ - In `validation="off"` mode, `fields=None` returns full payloads (API-default)
176
+ - `fields="all"` or `fields="*"` requests full payloads.
177
+ - `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.)
178
+ may be populated later by Lexicon.
179
+
180
+ ### Track Search, Filters, and Sort
181
+
182
+ `tracks.search(filter=...)` accepts a dict of field names and values. The SDK
183
+ validates fields/values in `"warn"`/`"strict"` modes and can send API-native values
184
+ in `"off"` mode.
185
+
186
+ Examples:
187
+
188
+ ```python
189
+ # text filters
190
+ lex.tracks.search({"artist": "Daft Punk"})
191
+
192
+ # numeric filters (strings)
193
+ lex.tracks.search({"bpm": "120"})
194
+ lex.tracks.search({"bpm": "120-128"})
195
+ lex.tracks.search({"bpm": ">=120"})
196
+
197
+ # date filters (YYYY-MM-DD)
198
+ lex.tracks.search({"dateAdded": "2024-01-01"})
199
+
200
+ # tag filters (comma-separated names)
201
+ # - default: OR across tags
202
+ # - prefix with "~" to require ALL tags (AND)
203
+ # - prefix with "!" to exclude a tag
204
+ lex.tracks.search({"tags": "Rock, Chill"}) # Rock OR Chill
205
+ lex.tracks.search({"tags": "~Rock, Chill"}) # Rock AND Chill
206
+ lex.tracks.search({"tags": "~Rock, !Chill"}) # Rock AND NOT Chill
207
+ ```
208
+
209
+ Tag filter details:
210
+
211
+ - Input is a single string with comma-separated tag names.
212
+ - Whitespace is ignored around commas.
213
+ - `~` at the start switches from OR to AND for the list.
214
+ - `!` before a tag name negates that tag.
215
+ - There is no supported way to search for “no tags”; `NONE` is not accepted.
216
+
217
+ Sort can be expressed in two shapes:
218
+
219
+ - API-native: list of dicts: `[{"field": "title", "dir": "asc"}]`
220
+ - Alternative: list of tuples: `[("title", "asc")]`
221
+
222
+ API-native dicts work in all modes; `validation="off"` requires the dict shape.
223
+
224
+ ## Playlists
225
+
226
+ ```python
227
+ playlist = lex.playlists.get(42)
228
+ playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="playlist")
229
+ tree = lex.playlists.list()
230
+
231
+ new_id = lex.playlists.add("Demo Playlist", playlist_type="playlist", parent_id=1)
232
+ lex.playlists.update(new_id, name="Renamed Playlist")
233
+ lex.playlists.delete([new_id])
234
+ ```
235
+
236
+ Playlist type accepts:
237
+
238
+ - `"folder"`, `"playlist"`, `"smartlist"`
239
+ - `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
240
+
241
+ ### Playlist tracks helpers
242
+ For getting the tracks of a playlist or editing the tracklist.
243
+
244
+ ```python
245
+ track_ids = lex.playlists.tracks.list(42)
246
+ tracks = lex.playlists.tracks.get(42)
247
+ lex.playlists.tracks.add(42, [1, 2, 3])
248
+ lex.playlists.tracks.remove(42, [1, 2])
249
+ lex.playlists.tracks.update(42, [3, 2, 1])
250
+ ```
251
+
252
+ ## Tags
253
+
254
+ ```python
255
+ tags = lex.tags.list()
256
+ new_tag = lex.tags.add(category_id=1, label="Demo Tag")
257
+ lex.tags.update(new_tag["id"], label="Renamed Tag")
258
+ lex.tags.delete(new_tag["id"])
259
+
260
+ categories = lex.tags.categories.list()
261
+ new_cat = lex.tags.categories.add(label="Demo Category", color="red")
262
+ lex.tags.categories.update(new_cat["id"], label="Renamed Category")
263
+ lex.tags.categories.delete(new_cat["id"])
264
+ ```
265
+
266
+ ## Tools
267
+
268
+ Interactive playlist chooser (requires InquirerPy):
269
+
270
+ ```python
271
+ choice = lex.playlists.choose()
272
+ print(choice)
273
+
274
+ # Or avoid an API call if you already have the tree
275
+ playlist_tree = lex.playlists.list()
276
+ choice = lex.tools.playlists.choose_playlist(playlist_tree) if playlist_tree else None
277
+ print(choice)
278
+ ```
279
+
280
+ Helper to resolve a playlist path from a tree:
281
+
282
+ ```python
283
+ path = lex.playlists.get_path(42)
284
+ print(path)
285
+
286
+ # Or avoid an API call if you already have the tree
287
+ playlist_tree = lex.playlists.list()
288
+ path = lex.tools.playlists.get_path_from_tree(playlist_tree, playlist_id=42)
289
+ print(path)
290
+
291
+ # -> ["Genres", "Drum & Bass"]
292
+ ```
293
+
294
+ ## Raw Requests (Escape Hatch)
295
+ The API can always be accessed directly with `lex.request`:
296
+
297
+ ```python
298
+ payload = lex.request("GET", "/tracks", params={"fields": "all"})
299
+ ```
300
+
301
+ ## Resource Overview
302
+
303
+ High level namespaces (full mapping in `docs/resource-map.md`):
304
+
305
+ - `lex.tracks`: get, get_many, list, search, add, update, delete
306
+ - `lex.playlists`: get, get_many, list (tree root), get_path, get_by_path, add, update, delete, choose
307
+ - `lex.playlists.tracks`: list (IDs), get (track dicts), add, remove, update
308
+ - `lex.tags`: list, add, update, delete
309
+ - `lex.tags.categories`: list, add, update, delete
310
+
311
+ ## Type Hints
312
+
313
+ The SDK includes TypedDict and Literal types for payloads and enums. These are
314
+ intended to improve editor autocomplete and static checks.
315
+
316
+ For full payload schemas and endpoint details, refer to the Lexicon API docs:
317
+
318
+ - https://www.lexicondj.com/docs/developers/api
319
+ - https://www.lexicondj.com/developer/api-docs.yaml
320
+
321
+ ## Development
322
+
323
+ ```bash
324
+ ./.venv/bin/python -m pytest --cov --cov-branch --cov-report=term-missing
325
+ ```
326
+
327
+ ## License
328
+
329
+ MIT (see `LICENSE`).
@@ -0,0 +1,314 @@
1
+ # Lexicon Python
2
+
3
+ Python client for the Lexicon DJ Local API.
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.
8
+
9
+ ## Features
10
+
11
+ - Resource grouped client (`lex.tracks`, `lex.playlists`, `lex.tags`)
12
+ - Pagination handled for `tracks.list()` (API returns 1000 per page)
13
+ - Validation modes for inputs: warn (default), strict, or off
14
+ - Typed responses and payload hints (TypedDicts and Literals)
15
+ - Optional interactive playlist chooser via `InquirerPy`
16
+ - Raw request escape hatch via `lex.request(...)`
17
+
18
+ ## Requirements
19
+
20
+ - Python 3.9+
21
+ - Lexicon DJ running with Local API enabled
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install lexicon-python
27
+ ```
28
+
29
+ Optional (for interactive playlist chooser):
30
+
31
+ ```bash
32
+ pip install InquirerPy
33
+ ```
34
+
35
+ ## Quickstart
36
+
37
+ ```python
38
+ from lexicon import Lexicon
39
+
40
+ lex = Lexicon()
41
+
42
+ # list tracks (default fields)
43
+ tracks = lex.tracks.list(limit=10) or []
44
+ for t in tracks:
45
+ print(t.get("artist"), "-", t.get("title"))
46
+
47
+ # search
48
+ results = lex.tracks.search({"artist": "Daft Punk"}) or []
49
+ print("matches:", len(results))
50
+
51
+ # get a playlist by path
52
+ playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
53
+ print(playlist)
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ By default the client targets:
59
+
60
+ - Host: `localhost`
61
+ - Port: `48624`
62
+
63
+ You can override these via constructor args or environment variables.
64
+
65
+ ```python
66
+ lex = Lexicon(host="127.0.0.1", port=48624, raise_on_error=False)
67
+ ```
68
+
69
+ Other options:
70
+
71
+ - `default_timeout`: request timeout in seconds
72
+ - `session`: optional `requests.Session`
73
+ - `raise_on_error`: raise HTTP errors instead of returning None
74
+
75
+ Environment variables:
76
+
77
+ ```bash
78
+ export LEXICON_HOST=localhost
79
+ export LEXICON_PORT=48624
80
+ ```
81
+
82
+ ## Validation Modes
83
+
84
+ Many methods accept a `validation` parameter with three modes:
85
+
86
+ - `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
87
+ failures, but intended changes may be ignored.
88
+ - `"strict"`: invalid inputs raise `ValueError`.
89
+ - `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
90
+
91
+ Example:
92
+
93
+ ```python
94
+ lex.tracks.search({"rating": "bad"}, validation="warn") # logs warning
95
+ lex.tracks.search({"rating": "bad"}, validation="strict") # raises
96
+ lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
97
+ ```
98
+
99
+ ## Convenience vs Raw API
100
+
101
+ This SDK adds several quality-of-life behaviors on top of the raw Lexicon API.
102
+ In general, response shapes are unwrapped (e.g., `"data": {...}` is removed and
103
+ single-item lists are collapsed to a single dict).
104
+
105
+ ### Additional Methods (Not in the Raw API)
106
+
107
+ - `tracks.get_many()` repeats `get()` and preserves input order.
108
+ - `playlists.get_many()` repeats `get()` and preserves input order.
109
+ - `playlists.tracks.get()` fetches track dicts in playlist order.
110
+ - `playlists.tracks.update()` replaces the full track list (remove + add).
111
+ - `playlists.choose()` provides an interactive chooser (wraps list + get).
112
+ - `playlists.get_path()` resolves a path from a playlist tree.
113
+
114
+ ### Input Normalization (Broader Accepted Inputs)
115
+
116
+ - **ID normalization**: Many methods accept lists of IDs instead of a single ID
117
+ for easier batch operations.
118
+ - **Field selection**: By default, `tracks.list()` and `tracks.search()` return a
119
+ minimal set of fields rather than the full payload:
120
+ - `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
121
+ - Fields used as a search filter or sort item are also returned.
122
+ - **Search filter normalization**:
123
+ - Text fields accept `None` (becomes `"NONE"` in filter context).
124
+ - Numeric filters accept `None` (becomes `"0"`).
125
+ - Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` /
126
+ `datetime.datetime` inputs (time is stripped).
127
+ - Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
128
+ - **Sort normalization**:
129
+ - Accepts tuple shorthand: `[("title", "asc")]`.
130
+ - **Track update helpers**:
131
+ - Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
132
+ name/number variants).
133
+ - Invalid entries can be skipped in `"warn"` mode without failing the update.
134
+
135
+ If no SDK normalization is desired, use `validation="off"` and pass
136
+ API-native payloads. API-native shapes are also accepted in `"warn"`/`"strict"`;
137
+ those modes simply add normalization/validation on top. For fully raw access,
138
+ `lex.request(...)` can always be called directly.
139
+
140
+ ## Tracks
141
+
142
+ Common operations:
143
+
144
+ ```python
145
+ track = lex.tracks.get(123)
146
+ tracks = lex.tracks.get_many([1, 2, 3])
147
+ tracks = lex.tracks.list(limit=100)
148
+ tracks = lex.tracks.search({"artist": "Daft Punk"})
149
+
150
+ added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
151
+ updated = lex.tracks.update(123, {"title": "New Title"})
152
+ lex.tracks.delete([123, 456])
153
+ ```
154
+
155
+ Notes:
156
+
157
+ - `tracks.search()` results are capped at 1000 by the API.
158
+ - `tracks.get_many()` preserves input order and returns `None` for missing IDs.
159
+ - `fields=None` returns a minimal default set of fields.
160
+ - In `validation="off"` mode, `fields=None` returns full payloads (API-default)
161
+ - `fields="all"` or `fields="*"` requests full payloads.
162
+ - `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.)
163
+ may be populated later by Lexicon.
164
+
165
+ ### Track Search, Filters, and Sort
166
+
167
+ `tracks.search(filter=...)` accepts a dict of field names and values. The SDK
168
+ validates fields/values in `"warn"`/`"strict"` modes and can send API-native values
169
+ in `"off"` mode.
170
+
171
+ Examples:
172
+
173
+ ```python
174
+ # text filters
175
+ lex.tracks.search({"artist": "Daft Punk"})
176
+
177
+ # numeric filters (strings)
178
+ lex.tracks.search({"bpm": "120"})
179
+ lex.tracks.search({"bpm": "120-128"})
180
+ lex.tracks.search({"bpm": ">=120"})
181
+
182
+ # date filters (YYYY-MM-DD)
183
+ lex.tracks.search({"dateAdded": "2024-01-01"})
184
+
185
+ # tag filters (comma-separated names)
186
+ # - default: OR across tags
187
+ # - prefix with "~" to require ALL tags (AND)
188
+ # - prefix with "!" to exclude a tag
189
+ lex.tracks.search({"tags": "Rock, Chill"}) # Rock OR Chill
190
+ lex.tracks.search({"tags": "~Rock, Chill"}) # Rock AND Chill
191
+ lex.tracks.search({"tags": "~Rock, !Chill"}) # Rock AND NOT Chill
192
+ ```
193
+
194
+ Tag filter details:
195
+
196
+ - Input is a single string with comma-separated tag names.
197
+ - Whitespace is ignored around commas.
198
+ - `~` at the start switches from OR to AND for the list.
199
+ - `!` before a tag name negates that tag.
200
+ - There is no supported way to search for “no tags”; `NONE` is not accepted.
201
+
202
+ Sort can be expressed in two shapes:
203
+
204
+ - API-native: list of dicts: `[{"field": "title", "dir": "asc"}]`
205
+ - Alternative: list of tuples: `[("title", "asc")]`
206
+
207
+ API-native dicts work in all modes; `validation="off"` requires the dict shape.
208
+
209
+ ## Playlists
210
+
211
+ ```python
212
+ playlist = lex.playlists.get(42)
213
+ playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="playlist")
214
+ tree = lex.playlists.list()
215
+
216
+ new_id = lex.playlists.add("Demo Playlist", playlist_type="playlist", parent_id=1)
217
+ lex.playlists.update(new_id, name="Renamed Playlist")
218
+ lex.playlists.delete([new_id])
219
+ ```
220
+
221
+ Playlist type accepts:
222
+
223
+ - `"folder"`, `"playlist"`, `"smartlist"`
224
+ - `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
225
+
226
+ ### Playlist tracks helpers
227
+ For getting the tracks of a playlist or editing the tracklist.
228
+
229
+ ```python
230
+ track_ids = lex.playlists.tracks.list(42)
231
+ tracks = lex.playlists.tracks.get(42)
232
+ lex.playlists.tracks.add(42, [1, 2, 3])
233
+ lex.playlists.tracks.remove(42, [1, 2])
234
+ lex.playlists.tracks.update(42, [3, 2, 1])
235
+ ```
236
+
237
+ ## Tags
238
+
239
+ ```python
240
+ tags = lex.tags.list()
241
+ new_tag = lex.tags.add(category_id=1, label="Demo Tag")
242
+ lex.tags.update(new_tag["id"], label="Renamed Tag")
243
+ lex.tags.delete(new_tag["id"])
244
+
245
+ categories = lex.tags.categories.list()
246
+ new_cat = lex.tags.categories.add(label="Demo Category", color="red")
247
+ lex.tags.categories.update(new_cat["id"], label="Renamed Category")
248
+ lex.tags.categories.delete(new_cat["id"])
249
+ ```
250
+
251
+ ## Tools
252
+
253
+ Interactive playlist chooser (requires InquirerPy):
254
+
255
+ ```python
256
+ choice = lex.playlists.choose()
257
+ print(choice)
258
+
259
+ # Or avoid an API call if you already have the tree
260
+ playlist_tree = lex.playlists.list()
261
+ choice = lex.tools.playlists.choose_playlist(playlist_tree) if playlist_tree else None
262
+ print(choice)
263
+ ```
264
+
265
+ Helper to resolve a playlist path from a tree:
266
+
267
+ ```python
268
+ path = lex.playlists.get_path(42)
269
+ print(path)
270
+
271
+ # Or avoid an API call if you already have the tree
272
+ playlist_tree = lex.playlists.list()
273
+ path = lex.tools.playlists.get_path_from_tree(playlist_tree, playlist_id=42)
274
+ print(path)
275
+
276
+ # -> ["Genres", "Drum & Bass"]
277
+ ```
278
+
279
+ ## Raw Requests (Escape Hatch)
280
+ The API can always be accessed directly with `lex.request`:
281
+
282
+ ```python
283
+ payload = lex.request("GET", "/tracks", params={"fields": "all"})
284
+ ```
285
+
286
+ ## Resource Overview
287
+
288
+ High level namespaces (full mapping in `docs/resource-map.md`):
289
+
290
+ - `lex.tracks`: get, get_many, list, search, add, update, delete
291
+ - `lex.playlists`: get, get_many, list (tree root), get_path, get_by_path, add, update, delete, choose
292
+ - `lex.playlists.tracks`: list (IDs), get (track dicts), add, remove, update
293
+ - `lex.tags`: list, add, update, delete
294
+ - `lex.tags.categories`: list, add, update, delete
295
+
296
+ ## Type Hints
297
+
298
+ The SDK includes TypedDict and Literal types for payloads and enums. These are
299
+ intended to improve editor autocomplete and static checks.
300
+
301
+ For full payload schemas and endpoint details, refer to the Lexicon API docs:
302
+
303
+ - https://www.lexicondj.com/docs/developers/api
304
+ - https://www.lexicondj.com/developer/api-docs.yaml
305
+
306
+ ## Development
307
+
308
+ ```bash
309
+ ./.venv/bin/python -m pytest --cov --cov-branch --cov-report=term-missing
310
+ ```
311
+
312
+ ## License
313
+
314
+ MIT (see `LICENSE`).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lexicon-python"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "Python client for the Lexicon DJ API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -14,8 +14,9 @@ authors = [
14
14
  {name = "Garrison Burger", email = "burgerga123@gmail.com"}
15
15
  ]
16
16
  dependencies = [
17
+ "InquirerPy>=0.3",
17
18
  "requests>=2.31",
18
- "tqdm>=4.0"
19
+ "typing_extensions>=4.0"
19
20
  ]
20
21
 
21
22
  [project.urls]
@@ -26,3 +27,17 @@ where = ["src"]
26
27
 
27
28
  [tool.setuptools]
28
29
  package-dir = {"" = "src"}
30
+
31
+ [tool.pydocstyle]
32
+ convention = "numpy"
33
+
34
+ [tool.coverage.run]
35
+ omit = [
36
+ "tests/*",
37
+ ]
38
+
39
+ [tool.coverage.report]
40
+ exclude_lines = [
41
+ "pragma: no cover",
42
+ "if __name__ == .__main__.:",
43
+ ]
@@ -0,0 +1,8 @@
1
+ """Public package surface for the lexicon Python client."""
2
+
3
+ from .client import DEFAULT_HOST, LEXICON_PORT, Lexicon
4
+ from .resources.tracks_types import *
5
+
6
+
7
+
8
+ __all__ = ["DEFAULT_HOST", "LEXICON_PORT", "Lexicon"]