lexicon-python 0.2.0__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.
- {lexicon_python-0.2.0/src/lexicon_python.egg-info → lexicon_python-0.3.0}/PKG-INFO +73 -9
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/README.md +71 -7
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/pyproject.toml +22 -2
- lexicon_python-0.3.0/src/lexicon/__init__.py +23 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/client.py +19 -4
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/_common_types.py +102 -53
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/base.py +3 -1
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/playlist_tracks.py +17 -13
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/playlists.py +121 -31
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/playlists_types.py +39 -19
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/tag_categories.py +28 -21
- lexicon_python-0.3.0/src/lexicon/resources/tag_categories_types.py +23 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/tags.py +10 -6
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/tags_types.py +12 -6
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/tracks.py +201 -46
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/tracks_types.py +361 -105
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/tools/__init__.py +0 -2
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/tools/playlists.py +5 -1
- lexicon_python-0.3.0/src/lexicon/tools/tracks.py +130 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/utils.py +1 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0/src/lexicon_python.egg-info}/PKG-INFO +73 -9
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon_python.egg-info/SOURCES.txt +2 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_base.py +7 -2
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_client.py +3 -2
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_common_types.py +1 -1
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_playlist_tracks.py +25 -10
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_playlists.py +173 -36
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_playlists_types.py +4 -1
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_tag_categories.py +42 -19
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_tags.py +4 -1
- lexicon_python-0.3.0/tests/test_tools_tracks.py +103 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_tracks.py +523 -117
- lexicon_python-0.2.0/src/lexicon/__init__.py +0 -8
- lexicon_python-0.2.0/src/lexicon/resources/tag_categories_types.py +0 -18
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/LICENSE +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/setup.cfg +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/__init__.py +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon/resources/playlist_tracks_types.py +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon_python.egg-info/dependency_links.txt +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon_python.egg-info/requires.txt +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/src/lexicon_python.egg-info/top_level.txt +0 -0
- {lexicon_python-0.2.0 → lexicon_python-0.3.0}/tests/test_tools_playlists.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lexicon-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python client for the Lexicon DJ API
|
|
5
5
|
Author-email: Garrison Burger <burgerga123@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/photonicvelocity/lexicon-python
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: InquirerPy>=0.3
|
|
@@ -41,12 +41,6 @@ management, and metadata edits while keeping a clean escape hatch to the raw API
|
|
|
41
41
|
pip install lexicon-python
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Optional (for interactive playlist chooser):
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
pip install InquirerPy
|
|
48
|
-
```
|
|
49
|
-
|
|
50
44
|
## Quickstart
|
|
51
45
|
|
|
52
46
|
```python
|
|
@@ -320,10 +314,80 @@ For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
|
320
314
|
|
|
321
315
|
## Development
|
|
322
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
|
+
|
|
323
342
|
```bash
|
|
324
|
-
|
|
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
|
|
325
350
|
```
|
|
326
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
|
+
|
|
327
391
|
## License
|
|
328
392
|
|
|
329
393
|
MIT (see `LICENSE`).
|
|
@@ -26,12 +26,6 @@ management, and metadata edits while keeping a clean escape hatch to the raw API
|
|
|
26
26
|
pip install lexicon-python
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Optional (for interactive playlist chooser):
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
pip install InquirerPy
|
|
33
|
-
```
|
|
34
|
-
|
|
35
29
|
## Quickstart
|
|
36
30
|
|
|
37
31
|
```python
|
|
@@ -305,10 +299,80 @@ For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
|
305
299
|
|
|
306
300
|
## Development
|
|
307
301
|
|
|
302
|
+
### Setup
|
|
303
|
+
|
|
304
|
+
#### Pip
|
|
305
|
+
|
|
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:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
python -m venv .venv
|
|
311
|
+
source .venv/bin/activate
|
|
312
|
+
pip install -e ".[dev]"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### uv
|
|
316
|
+
|
|
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:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
uv sync --dev
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The lockfile (`uv.lock`) is checked in to ensure reproducible installs.
|
|
324
|
+
|
|
325
|
+
### Running Tests
|
|
326
|
+
|
|
308
327
|
```bash
|
|
309
|
-
|
|
328
|
+
# Unit tests
|
|
329
|
+
make run-tests
|
|
330
|
+
|
|
331
|
+
# Integration tests (requires Lexicon running)
|
|
332
|
+
# Note: Integration tests enforce an empty library state to avoid destructive edits on existing libraries.
|
|
333
|
+
# The fixture setup will back up the existing library, clear it for testing, and restore it afterward.
|
|
334
|
+
make run-integration-tests
|
|
310
335
|
```
|
|
311
336
|
|
|
337
|
+
### Linting and Formatting
|
|
338
|
+
|
|
339
|
+
The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and
|
|
340
|
+
formatting.
|
|
341
|
+
|
|
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.
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
make test # format-fix → lint-fix → format-check → lint-check → tests
|
|
347
|
+
make fix # lint-fix → format-fix
|
|
348
|
+
make clean # remove __pycache__, .pytest_cache, .ruff_cache, etc.
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
If you want to run the linters or formatters manually, you can use the following commands:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
make lint-check # check for lint issues
|
|
355
|
+
make lint-fix # auto-fix lint issues
|
|
356
|
+
make format-check # check formatting
|
|
357
|
+
make format-fix # auto-fix formatting
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### CI
|
|
361
|
+
|
|
362
|
+
GitHub Actions runs on every push to `main` and on pull requests
|
|
363
|
+
targeting those branches. The pipeline includes:
|
|
364
|
+
|
|
365
|
+
- **Tests** across Python 3.9, 3.10, 3.11, and 3.12
|
|
366
|
+
- **Lint and format checks** via ruff on Python 3.12
|
|
367
|
+
|
|
368
|
+
Before opening a PR, make sure `make test` passes locally.
|
|
369
|
+
|
|
370
|
+
### Pull Requests
|
|
371
|
+
|
|
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.
|
|
375
|
+
|
|
312
376
|
## License
|
|
313
377
|
|
|
314
378
|
MIT (see `LICENSE`).
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lexicon-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Python client for the Lexicon DJ API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
|
-
requires-python = ">=3.
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
13
|
authors = [
|
|
14
14
|
{name = "Garrison Burger", email = "burgerga123@gmail.com"}
|
|
15
15
|
]
|
|
@@ -19,6 +19,15 @@ dependencies = [
|
|
|
19
19
|
"typing_extensions>=4.0"
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=7.0",
|
|
25
|
+
"pytest-cov>=4.0",
|
|
26
|
+
"ruff>=0.1.0",
|
|
27
|
+
"mypy>=1.0",
|
|
28
|
+
"ipython>=8.12.3",
|
|
29
|
+
]
|
|
30
|
+
|
|
22
31
|
[project.urls]
|
|
23
32
|
Homepage = "https://github.com/photonicvelocity/lexicon-python"
|
|
24
33
|
|
|
@@ -28,6 +37,17 @@ where = ["src"]
|
|
|
28
37
|
[tool.setuptools]
|
|
29
38
|
package-dir = {"" = "src"}
|
|
30
39
|
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
markers = [
|
|
43
|
+
"integration: integration tests requiring a running Lexicon instance",
|
|
44
|
+
]
|
|
45
|
+
addopts = "-m 'not integration'"
|
|
46
|
+
|
|
47
|
+
[tool.uv]
|
|
48
|
+
# Dev dependencies are specified in [dependency-groups]
|
|
49
|
+
default-groups = ["dev"]
|
|
50
|
+
|
|
31
51
|
[tool.pydocstyle]
|
|
32
52
|
convention = "numpy"
|
|
33
53
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Public package surface for the lexicon Python client."""
|
|
2
|
+
|
|
3
|
+
from .client import DEFAULT_HOST, LEXICON_PORT, Lexicon
|
|
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
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DEFAULT_HOST",
|
|
15
|
+
"LEXICON_PORT",
|
|
16
|
+
"Lexicon",
|
|
17
|
+
"TrackResponse",
|
|
18
|
+
"PlaylistResponse",
|
|
19
|
+
"TagResponse",
|
|
20
|
+
"TagCategoryResponse",
|
|
21
|
+
"CuePointResponse",
|
|
22
|
+
"TempoMarkerResponse",
|
|
23
|
+
]
|
|
@@ -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:
|
|
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(
|
|
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 =
|
|
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",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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,
|
|
33
|
-
(230,
|
|
34
|
-
(242, 102,
|
|
35
|
-
(239,
|
|
36
|
-
(232, 137,
|
|
37
|
-
(255, 225, 148),
|
|
38
|
-
(245, 208,
|
|
39
|
-
(245, 245,
|
|
40
|
-
(186, 232,
|
|
41
|
-
(174, 245,
|
|
42
|
-
(127, 231,
|
|
43
|
-
(
|
|
44
|
-
(
|
|
45
|
-
(
|
|
46
|
-
(
|
|
47
|
-
(
|
|
48
|
-
(
|
|
49
|
-
(
|
|
50
|
-
(132,
|
|
51
|
-
(170,
|
|
52
|
-
(198, 140, 243),
|
|
53
|
-
(230,
|
|
54
|
-
(170,
|
|
55
|
-
(222,
|
|
56
|
-
(173, 173, 173),
|
|
57
|
-
(
|
|
58
|
-
(
|
|
59
|
-
(255, 255, 255),
|
|
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
|
|
64
|
-
"""
|
|
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
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = [
|
|
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
|
|
|
@@ -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(
|
|
30
|
+
return self._client.request(
|
|
31
|
+
method, path, params=params, json=json, timeout=timeout
|
|
32
|
+
)
|
|
31
33
|
|
|
32
34
|
def _get(
|
|
33
35
|
self,
|