lexicon-python 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,8 +1,23 @@
1
1
  """Public package surface for the lexicon Python client."""
2
2
 
3
3
  from .client import DEFAULT_HOST, LEXICON_PORT, Lexicon
4
- from .resources.tracks_types import *
4
+ from .resources.playlists_types import PlaylistResponse
5
+ from .resources.tag_categories_types import TagCategoryResponse
6
+ from .resources.tags_types import TagResponse
7
+ from .resources.tracks_types import (
8
+ CuePointResponse,
9
+ TempoMarkerResponse,
10
+ TrackResponse,
11
+ )
5
12
 
6
-
7
-
8
- __all__ = ["DEFAULT_HOST", "LEXICON_PORT", "Lexicon"]
13
+ __all__ = [
14
+ "DEFAULT_HOST",
15
+ "LEXICON_PORT",
16
+ "Lexicon",
17
+ "TrackResponse",
18
+ "PlaylistResponse",
19
+ "TagResponse",
20
+ "TagCategoryResponse",
21
+ "CuePointResponse",
22
+ "TempoMarkerResponse",
23
+ ]
lexicon/client.py CHANGED
@@ -14,17 +14,26 @@ from .resources.tag_categories import TagCategories
14
14
  from .resources.tags import Tags
15
15
  from .resources.tracks import Tracks
16
16
  from .tools import playlists as playlist_tools
17
+ from .tools import tracks as track_tools
18
+
17
19
  DEFAULT_HOST = os.environ.get("LEXICON_HOST", "localhost")
18
20
  LEXICON_PORT = int(os.environ.get("LEXICON_PORT", "48624"))
19
21
 
20
22
 
23
+ class Tools:
24
+ """Namespace for helper tools."""
25
+
26
+ playlists = playlist_tools
27
+ tracks = track_tools
28
+
29
+
21
30
  class Lexicon:
22
31
  """Resource-grouped client for the Lexicon Local API."""
23
32
 
24
33
  tracks: Tracks
25
34
  playlists: Playlists
26
35
  tags: Tags
27
- tools: Any
36
+ tools: Tools
28
37
 
29
38
  def __init__(
30
39
  self,
@@ -34,6 +43,7 @@ class Lexicon:
34
43
  default_timeout: int = 20,
35
44
  session: Optional[requests.Session] = None,
36
45
  raise_on_error: bool = False,
46
+ raw_enums: bool = True,
37
47
  ) -> None:
38
48
  """Create a Lexicon client bound to an API instance.
39
49
 
@@ -49,21 +59,26 @@ class Lexicon:
49
59
  Optional requests session to reuse connections.
50
60
  raise_on_error
51
61
  If True, raise HTTP errors instead of returning None.
62
+ raw_enums
63
+ If True (default), return API enum values as raw codes (e.g. ``"1"``).
64
+ If False, convert to human-readable names (e.g. ``"normal"``).
52
65
  """
53
66
  self.host = host or DEFAULT_HOST
54
67
  self.port = int(port or LEXICON_PORT)
55
68
  self.default_timeout = default_timeout
56
69
  self.raise_on_error = raise_on_error
70
+ self.raw_enums = raw_enums
57
71
  self._logger = logging.getLogger(__name__)
58
72
  self._session = session
59
73
 
60
74
  self.tracks: Tracks = Tracks(self)
61
75
  self.playlists: Playlists = Playlists(self)
62
- self.playlists.tracks = PlaylistTracks(self, tracks=self.tracks, playlists=self.playlists)
76
+ self.playlists.tracks = PlaylistTracks(
77
+ self, tracks=self.tracks, playlists=self.playlists
78
+ )
63
79
  self.tags: Tags = Tags(self)
64
80
  self.tags.categories = TagCategories(self)
65
- self.tools = type("Tools", (), {})()
66
- self.tools.playlists = playlist_tools
81
+ self.tools = Tools()
67
82
 
68
83
  def request(
69
84
  self,
@@ -22,62 +22,85 @@ ValidationMode = Literal["off", "warn", "strict"]
22
22
 
23
23
  # --- Color Types and Normalization --- #
24
24
  Color = Literal[
25
- "red_dark", "red", "red_light", "red_orange", "orange", "beige", "yellow_dark", "yellow",
26
- "lime", "green_light", "green", "green_dark", "teal", "aqua", "aqua_dark", "blue_light",
27
- "blue", "blue_dark", "blue_violet", "violet", "violet_light", "magenta", "magenta_dark",
28
- "magenta_red", "grey_light", "grey_dark", "black", "white",
25
+ "red_dark",
26
+ "red",
27
+ "red_light",
28
+ "red_orange",
29
+ "orange",
30
+ "beige",
31
+ "yellow_dark",
32
+ "yellow",
33
+ "lime",
34
+ "green_light",
35
+ "green",
36
+ "green_dark",
37
+ "teal",
38
+ "aqua",
39
+ "aqua_dark",
40
+ "blue_light",
41
+ "blue",
42
+ "blue_dark",
43
+ "blue_violet",
44
+ "violet",
45
+ "violet_light",
46
+ "magenta",
47
+ "magenta_dark",
48
+ "magenta_red",
49
+ "grey_light",
50
+ "grey_dark",
51
+ "black",
52
+ "white",
29
53
  ]
30
54
  COLORS: tuple[Color, ...] = get_args(Color)
31
55
  COLOR_RGBS: tuple[tuple[int, int, int], ...] = (
32
- (158, 15, 7), # red_dark
33
- (230, 15, 13), # red
34
- (242, 102, 92), # red_light
35
- (239, 89, 15), # red_orange
36
- (232, 137, 20), # orange
37
- (255, 225, 148), # beige
38
- (245, 208, 1), # yellow_dark
39
- (245, 245, 10), # yellow
40
- (186, 232, 22), # lime
41
- (174, 245, 95), # green_light
42
- (127, 231, 16), # green
43
- ( 75, 140, 8), # green_dark
44
- ( 20, 222, 120), # teal
45
- ( 20, 222, 202), # aqua
46
- ( 7, 148, 134), # aqua_dark
47
- ( 47, 168, 237), # blue_light
48
- ( 14, 88, 222), # blue
49
- ( 0, 40, 171), # blue_dark
50
- (132, 0, 255), # blue_violet
51
- (170, 59, 255), # violet
52
- (198, 140, 243), # violet_light
53
- (230, 15, 222), # magenta
54
- (170, 0, 188), # magenta_dark
55
- (222, 17, 92), # magenta_red
56
- (173, 173, 173), # grey_light
57
- ( 92, 92, 92), # grey_dark
58
- ( 48, 48, 48), # black
59
- (255, 255, 255), # white
56
+ (158, 15, 7), # red_dark
57
+ (230, 15, 13), # red
58
+ (242, 102, 92), # red_light
59
+ (239, 89, 15), # red_orange
60
+ (232, 137, 20), # orange
61
+ (255, 225, 148), # beige
62
+ (245, 208, 1), # yellow_dark
63
+ (245, 245, 10), # yellow
64
+ (186, 232, 22), # lime
65
+ (174, 245, 95), # green_light
66
+ (127, 231, 16), # green
67
+ (75, 140, 8), # green_dark
68
+ (20, 222, 120), # teal
69
+ (20, 222, 202), # aqua
70
+ (7, 148, 134), # aqua_dark
71
+ (47, 168, 237), # blue_light
72
+ (14, 88, 222), # blue
73
+ (0, 40, 171), # blue_dark
74
+ (132, 0, 255), # blue_violet
75
+ (170, 59, 255), # violet
76
+ (198, 140, 243), # violet_light
77
+ (230, 15, 222), # magenta
78
+ (170, 0, 188), # magenta_dark
79
+ (222, 17, 92), # magenta_red
80
+ (173, 173, 173), # grey_light
81
+ (92, 92, 92), # grey_dark
82
+ (48, 48, 48), # black
83
+ (255, 255, 255), # white
60
84
  )
61
85
 
62
86
 
63
- def _normalize_color(value: object) -> Color | None:
64
- """Normalize color inputs to the nearest Lexicon color name.
87
+ def _parse_color_rgb(value: object) -> tuple[int, int, int] | None:
88
+ """Parse a color input into an RGB tuple.
65
89
 
66
90
  Parameters
67
91
  ----------
68
92
  value
69
93
  Color input. Supported forms:
70
- - ``None`` or the string ``"None"`` (case-insensitive)
71
- - Lexicon color names (see ``COLORS``)
72
- - Various RGB/RGBA forms (alpha is dropped)
73
- - Hex strings (``#RGB``, ``#RGBA``, ``#RRGGBB``, ``#RRGGBBAA``)
74
- - RGB/RGBA tuples or lists (ints 0-255 or floats 0-1)
75
- - Packed RGB integer (``0xRRGGBB``, ``0xAARRGGBB``)
94
+ - ``None`` or the string ``"None"`` (case-insensitive) -> returns None
95
+ - Lexicon color names (see ``COLORS``) -> looked up from COLOR_RGBS
96
+ - Hex strings (``#RGB``, ``#RGBA``, ``#RRGGBB``, ``#RRGGBBAA``)
97
+ - RGB/RGBA tuples or lists (ints 0-255 or floats 0-1)
98
+ - Packed RGB integer (``0xRRGGBB``, ``0xAARRGGBB``)
76
99
 
77
100
  Returns
78
101
  -------
79
- Color or None
80
- The nearest Lexicon color name, or ``None`` when the input is ``None``.
102
+ tuple[int, int, int] or None
103
+ RGB tuple (0-255), or ``None`` when the input is ``None``.
81
104
 
82
105
  Raises
83
106
  ------
@@ -90,20 +113,23 @@ def _normalize_color(value: object) -> Color | None:
90
113
  if value.strip().lower() == "none":
91
114
  return None
92
115
  if value in COLORS:
93
- return value
94
- hex_match = re.match(r"^\s*#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})\s*$", value, flags=re.IGNORECASE)
116
+ return COLOR_RGBS[COLORS.index(value)]
117
+ hex_match = re.match(
118
+ r"^\s*#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})\s*$",
119
+ value,
120
+ flags=re.IGNORECASE,
121
+ )
95
122
  if hex_match:
96
123
  hex_value = hex_match.group(1)
97
124
  if len(hex_value) in (3, 4):
98
125
  hex_value = "".join(ch * 2 for ch in hex_value)
99
126
  if len(hex_value) == 8:
100
127
  hex_value = hex_value[:6]
101
- rgb = (
128
+ return (
102
129
  int(hex_value[0:2], 16),
103
130
  int(hex_value[2:4], 16),
104
131
  int(hex_value[4:6], 16),
105
132
  )
106
- return _nearest_color(rgb)
107
133
  raise ValueError(f"Unsupported string input {value!r}")
108
134
 
109
135
  if isinstance(value, int):
@@ -111,8 +137,7 @@ def _normalize_color(value: object) -> Color | None:
111
137
  raise ValueError(f"Negative packed int {value!r}")
112
138
  if value > 0xFFFFFF:
113
139
  value = value & 0xFFFFFF
114
- rgb = ((value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF)
115
- return _nearest_color(rgb)
140
+ return ((value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF)
116
141
 
117
142
  if isinstance(value, (tuple, list)) and len(value) in (3, 4):
118
143
  rgb_values = value[:3]
@@ -123,12 +148,34 @@ def _normalize_color(value: object) -> Color | None:
123
148
  if channel_value <= 1:
124
149
  channel_value *= 255
125
150
  rgb.append(int(max(0, min(255, round(channel_value)))))
126
- return _nearest_color((rgb[0], rgb[1], rgb[2]))
151
+ return (rgb[0], rgb[1], rgb[2])
127
152
  raise ValueError(f"Invalid RGB tuple values {value!r}")
128
153
 
129
154
  raise ValueError(f"Unsupported input type {type(value)}")
130
155
 
131
156
 
157
+ def _normalize_color(value: object) -> Color | None:
158
+ """Normalize color inputs to the nearest Lexicon color name.
159
+
160
+ Used for tracks and cuepoints which use a fixed color swatch.
161
+ """
162
+ rgb = _parse_color_rgb(value)
163
+ if rgb is None:
164
+ return None
165
+ return _nearest_color(rgb)
166
+
167
+
168
+ def _normalize_color_hex(value: object) -> str | None:
169
+ """Normalize color inputs to a hex string (#RRGGBB).
170
+
171
+ Used for tag categories which support a full color picker.
172
+ """
173
+ rgb = _parse_color_rgb(value)
174
+ if rgb is None:
175
+ return None
176
+ return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
177
+
178
+
132
179
  def _nearest_color(rgb: tuple[int, int, int]) -> Color:
133
180
  """Find the nearest Lexicon color to the given RGB values."""
134
181
  best_index = 0
@@ -147,19 +194,19 @@ def _nearest_color(rgb: tuple[int, int, int]) -> Color:
147
194
  # --- ID Sequence Normalization --- #
148
195
  def _normalize_id_sequence(ids: int | Sequence[int] | object) -> list[int] | None:
149
196
  """Normalize single ID or sequence of IDs to a deduplicated list.
150
-
197
+
151
198
  Parameters
152
199
  ----------
153
200
  ids
154
201
  Single integer ID or sequence of integer IDs.
155
-
202
+
156
203
  Returns
157
204
  -------
158
205
  list[int] | None
159
206
  Deduplicated list of valid IDs (>= 1), or None if:
160
207
  - Input is not int or sequence (or is str/bytes)
161
208
  - No valid IDs found (all < 1)
162
-
209
+
163
210
  Notes
164
211
  -----
165
212
  Used for normalizing track IDs, tag IDs, and similar integer sequences.
@@ -172,7 +219,9 @@ def _normalize_id_sequence(ids: int | Sequence[int] | object) -> list[int] | Non
172
219
  else:
173
220
  return None
174
221
 
175
- valid_ids = [id_val for id_val in id_list if isinstance(id_val, int) and id_val >= 1]
222
+ valid_ids = [
223
+ id_val for id_val in id_list if isinstance(id_val, int) and id_val >= 1
224
+ ]
176
225
  if not valid_ids:
177
226
  return None
178
227
 
lexicon/resources/base.py CHANGED
@@ -27,7 +27,9 @@ class Resource:
27
27
  json: Optional[dict[str, Any]] = None,
28
28
  timeout: Optional[int] = None,
29
29
  ) -> Optional[dict[str, Any] | list[Any]]:
30
- return self._client.request(method, path, params=params, json=json, timeout=timeout)
30
+ return self._client.request(
31
+ method, path, params=params, json=json, timeout=timeout
32
+ )
31
33
 
32
34
  def _get(
33
35
  self,
@@ -10,7 +10,6 @@ from .playlists_types import PlaylistResponse
10
10
  from .tracks import Tracks
11
11
  from .tracks_types import TrackResponse
12
12
  from ._common_types import ValidationMode, _normalize_id_sequence
13
- from ..utils import unique_in_order
14
13
 
15
14
 
16
15
  class PlaylistTracks(Resource):
@@ -32,7 +31,6 @@ class PlaylistTracks(Resource):
32
31
  self._tracks = tracks
33
32
  self._playlists = playlists
34
33
 
35
-
36
34
  def get(
37
35
  self,
38
36
  playlist_id: int,
@@ -63,14 +61,13 @@ class PlaylistTracks(Resource):
63
61
  if validation == "warn":
64
62
  self._logger.warning("Invalid playlist_id for remove: %s", playlist_id)
65
63
  return None
66
-
64
+
67
65
  track_ids = self.list(playlist_id, validation=validation, timeout=timeout)
68
66
  if track_ids is None:
69
67
  return None
70
68
  if not track_ids:
71
69
  return []
72
70
  return self._tracks.get_many(track_ids, validation="off", timeout=timeout)
73
-
74
71
 
75
72
  def list(
76
73
  self,
@@ -112,7 +109,6 @@ class PlaylistTracks(Resource):
112
109
  return [track_id for track_id in track_ids if isinstance(track_id, int)]
113
110
  self._logger.warning("Playlist %s missing expected trackIds list", playlist_id)
114
111
  return None
115
-
116
112
 
117
113
  def add(
118
114
  self,
@@ -150,7 +146,7 @@ class PlaylistTracks(Resource):
150
146
  if validation == "warn":
151
147
  self._logger.warning("Invalid playlist_id for add: %s", playlist_id)
152
148
  return False
153
-
149
+
154
150
  # When validation is off, pass input directly without transformation
155
151
  if validation == "off":
156
152
  normalized_ids = [track_ids] if isinstance(track_ids, int) else track_ids
@@ -163,7 +159,7 @@ class PlaylistTracks(Resource):
163
159
  if validation == "warn": # pragma: no branch - strict raises above
164
160
  self._logger.warning("Invalid track_ids for add: %s", track_ids)
165
161
  return False
166
-
162
+
167
163
  if index is not None and (not isinstance(index, int) or index < 0):
168
164
  if validation == "strict":
169
165
  raise ValueError(f"Invalid index: {index}")
@@ -211,7 +207,7 @@ class PlaylistTracks(Resource):
211
207
  if validation == "warn":
212
208
  self._logger.warning("Invalid playlist_id for remove: %s", playlist_id)
213
209
  return False
214
-
210
+
215
211
  # When validation is off, pass input directly without transformation
216
212
  if validation == "off":
217
213
  normalized_ids = [track_ids] if isinstance(track_ids, int) else track_ids
@@ -224,7 +220,7 @@ class PlaylistTracks(Resource):
224
220
  if validation == "warn": # pragma: no branch - strict raises above
225
221
  self._logger.warning("Invalid track_ids for remove: %s", track_ids)
226
222
  return False
227
-
223
+
228
224
  payload = {"id": playlist_id, "trackIds": normalized_ids}
229
225
  response = self._delete("/playlist-tracks", json=payload, timeout=timeout)
230
226
  return response is not None
@@ -270,14 +266,20 @@ class PlaylistTracks(Resource):
270
266
  playlist_type = cast(PlaylistResponse, playlist).get("type")
271
267
  if playlist_type is not None and str(playlist_type) != "2":
272
268
  if validation == "strict":
273
- raise ValueError(f"Playlist {playlist_id} is not a normal playlist (type=2)")
269
+ raise ValueError(
270
+ f"Playlist {playlist_id} is not a normal playlist (type=2)"
271
+ )
274
272
  if validation == "warn":
275
- self._logger.warning("Playlist %s is not a normal playlist (type=2)", playlist_id)
273
+ self._logger.warning(
274
+ "Playlist %s is not a normal playlist (type=2)", playlist_id
275
+ )
276
276
  return False
277
277
 
278
278
  existing_ids = cast(PlaylistResponse, playlist).get("trackIds")
279
279
  if isinstance(existing_ids, list) and existing_ids:
280
- if not self.remove(playlist_id, existing_ids, validation="off", timeout=timeout):
280
+ if not self.remove(
281
+ playlist_id, existing_ids, validation="off", timeout=timeout
282
+ ):
281
283
  return False
282
284
 
283
285
  # When validation is off, pass input directly without transformation
@@ -295,4 +297,6 @@ class PlaylistTracks(Resource):
295
297
  if not normalized_ids:
296
298
  return True
297
299
 
298
- return self.add(playlist_id, normalized_ids, index=0, validation="off", timeout=timeout)
300
+ return self.add(
301
+ playlist_id, normalized_ids, index=0, validation="off", timeout=timeout
302
+ )