gukebox 1.0.0.dev12__tar.gz → 1.0.0.dev14__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 (121) hide show
  1. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/PKG-INFO +19 -21
  2. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/README.md +10 -7
  3. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/api/current_tag_router.py +6 -6
  4. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/api/discs_router.py +2 -4
  5. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/api/models.py +11 -11
  6. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/api/settings_router.py +5 -5
  7. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/api_controller.py +3 -5
  8. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/cli_controller.py +19 -21
  9. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/cli_display.py +2 -4
  10. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/interactive_cli_controller.py +20 -20
  11. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/ui_controller.py +44 -50
  12. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/ui_pages/library.py +16 -16
  13. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/ui_pages/settings.py +12 -10
  14. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/ui_pages/sonos.py +26 -27
  15. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/command_handlers.py +2 -1
  16. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/commands.py +12 -12
  17. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/edit_disc.py +3 -5
  18. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/get_current_tag_status.py +1 -3
  19. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/list_discs.py +1 -3
  20. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/resolve_tag_id.py +1 -3
  21. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/search_discs.py +1 -3
  22. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/inbound/config.py +11 -11
  23. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/json_library_adapter.py +9 -8
  24. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +1 -1
  25. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/players/sonos_player_adapter.py +38 -24
  26. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +5 -5
  27. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +5 -8
  28. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/sonos_discovery_adapter.py +7 -7
  29. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/text_current_tag_adapter.py +4 -5
  30. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/app.py +27 -27
  31. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/cli_presentation.py +43 -46
  32. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/command_handlers.py +11 -10
  33. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/commands.py +17 -17
  34. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/di_container.py +4 -6
  35. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/pn532_command_handlers.py +8 -7
  36. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/pn532_commands.py +2 -2
  37. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/di_container.py +29 -27
  38. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/entities/current_tag_action.py +2 -2
  39. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/entities/disc.py +4 -6
  40. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/entities/library.py +1 -3
  41. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/entities/playback_action.py +2 -2
  42. gukebox-1.0.0.dev14/jukebox/domain/entities/playback_session.py +17 -0
  43. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/entities/tag_event.py +1 -3
  44. gukebox-1.0.0.dev14/jukebox/domain/errors.py +2 -0
  45. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/ports/reader_port.py +1 -2
  46. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/repositories/current_tag_repository.py +1 -2
  47. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/repositories/library_repository.py +1 -2
  48. gukebox-1.0.0.dev14/jukebox/domain/use_cases/handle_tag_event.py +145 -0
  49. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/pn532/profiles.py +8 -8
  50. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/definitions.py +3 -2
  51. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/entities.py +50 -50
  52. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/file_settings_repository.py +2 -3
  53. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/migration.py +1 -2
  54. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/resolve.py +6 -6
  55. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/runtime_resolver.py +2 -2
  56. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/selected_sonos_group_repository.py +2 -4
  57. gukebox-1.0.0.dev14/jukebox/settings/types.py +4 -0
  58. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/validation_rules.py +3 -2
  59. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/view_utils.py +2 -2
  60. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/sonos/selection.py +8 -8
  61. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/sonos/service.py +3 -3
  62. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/pyproject.toml +15 -25
  63. gukebox-1.0.0.dev12/jukebox/domain/entities/playback_session.py +0 -19
  64. gukebox-1.0.0.dev12/jukebox/domain/use_cases/handle_tag_event.py +0 -121
  65. gukebox-1.0.0.dev12/jukebox/settings/types.py +0 -4
  66. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/LICENSE +0 -0
  67. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/__init__.py +0 -0
  68. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/__init__.py +0 -0
  69. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/__init__.py +0 -0
  70. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/api/__init__.py +0 -0
  71. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/inbound/ui_pages/__init__.py +0 -0
  72. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/outbound/__init__.py +0 -0
  73. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/outbound/json_library_adapter.py +0 -0
  74. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
  75. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/di_container.py +0 -0
  76. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/__init__.py +0 -0
  77. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/entities/__init__.py +0 -0
  78. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/entities/current_tag_status.py +0 -0
  79. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/repositories/__init__.py +0 -0
  80. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/__init__.py +0 -0
  81. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/add_disc.py +0 -0
  82. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/get_disc.py +0 -0
  83. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/discstore/domain/use_cases/remove_disc.py +0 -0
  84. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/__init__.py +0 -0
  85. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/__init__.py +0 -0
  86. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/inbound/__init__.py +0 -0
  87. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/inbound/cli_controller.py +0 -0
  88. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/__init__.py +0 -0
  89. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/players/__init__.py +0 -0
  90. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/adapters/outbound/readers/__init__.py +0 -0
  91. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/__init__.py +0 -0
  92. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/services.py +0 -0
  93. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/admin/sonos_households.py +0 -0
  94. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/app.py +0 -0
  95. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/__init__.py +0 -0
  96. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/entities/__init__.py +0 -0
  97. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/ports/__init__.py +0 -0
  98. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/ports/player_port.py +0 -0
  99. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/repositories/__init__.py +0 -0
  100. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/use_cases/__init__.py +0 -0
  101. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/use_cases/determine_action.py +0 -0
  102. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
  103. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/pn532/__init__.py +0 -0
  104. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/__init__.py +0 -0
  105. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/dict_utils.py +0 -0
  106. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/errors.py +0 -0
  107. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/repositories.py +0 -0
  108. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/runtime_validation.py +0 -0
  109. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/service_protocols.py +0 -0
  110. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/settings/timing_validation.py +0 -0
  111. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/shared/__init__.py +0 -0
  112. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/shared/config_utils.py +0 -0
  113. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/shared/dependency_messages.py +0 -0
  114. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/shared/logger.py +0 -0
  115. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/shared/terminal_ui.py +0 -0
  116. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/shared/timing.py +0 -0
  117. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/sonos/__init__.py +0 -0
  118. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/jukebox/sonos/discovery.py +0 -0
  119. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/pn532/__init__.py +0 -0
  120. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/pn532/pn532.py +0 -0
  121. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev14}/pn532/spi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gukebox
3
- Version: 1.0.0.dev12
3
+ Version: 1.0.0.dev14
4
4
  Summary: A Jukebox to play music on speakers using 'CD' with NFC tag
5
5
  Keywords: jukebox,music,nfc
6
6
  Author: Gudsfile
@@ -29,27 +29,22 @@ Classifier: Development Status :: 4 - Beta
29
29
  Classifier: License :: OSI Approved :: MIT License
30
30
  Classifier: Operating System :: OS Independent
31
31
  Classifier: Programming Language :: Python :: 3 :: Only
32
- Classifier: Programming Language :: Python :: 3.9
33
- Classifier: Programming Language :: Python :: 3.10
34
32
  Classifier: Programming Language :: Python :: 3.11
35
33
  Classifier: Programming Language :: Python :: 3.12
36
34
  Classifier: Programming Language :: Python :: 3.13
37
- Requires-Dist: pydantic==2.12.5
35
+ Requires-Dist: pydantic==2.13.3
38
36
  Requires-Dist: questionary==2.1.1
39
- Requires-Dist: soco==0.30.14
40
- Requires-Dist: typer==0.23.2 ; python_full_version < '3.10'
41
- Requires-Dist: typer==0.24.1 ; python_full_version >= '3.10'
42
- Requires-Dist: fastapi==0.128.7 ; python_full_version < '3.10' and extra == 'api'
43
- Requires-Dist: fastapi==0.135.1 ; python_full_version >= '3.10' and extra == 'api'
44
- Requires-Dist: uvicorn==0.39.0 ; python_full_version < '3.10' and extra == 'api'
45
- Requires-Dist: uvicorn==0.41.0 ; python_full_version >= '3.10' and extra == 'api'
37
+ Requires-Dist: soco==0.31.0
38
+ Requires-Dist: typer==0.24.1
39
+ Requires-Dist: fastapi==0.135.1 ; extra == 'api'
40
+ Requires-Dist: uvicorn==0.41.0 ; extra == 'api'
46
41
  Requires-Dist: pyserial==3.5 ; extra == 'pn532'
47
42
  Requires-Dist: spidev==3.8 ; extra == 'pn532'
48
43
  Requires-Dist: lgpio==0.2.2.0 ; python_full_version < '3.13' and extra == 'pn532'
49
44
  Requires-Dist: gukebox[api] ; extra == 'ui'
50
- Requires-Dist: fastui==0.9.0 ; python_full_version >= '3.10' and extra == 'ui'
51
- Requires-Dist: python-multipart==0.0.22 ; python_full_version >= '3.10' and extra == 'ui'
52
- Requires-Python: >=3.9, <3.14
45
+ Requires-Dist: fastui==0.9.0 ; extra == 'ui'
46
+ Requires-Dist: python-multipart==0.0.26 ; extra == 'ui'
47
+ Requires-Python: >=3.11, <3.14
53
48
  Project-URL: Repository, https://github.com/Gudsfile/jukebox
54
49
  Provides-Extra: api
55
50
  Provides-Extra: pn532
@@ -91,13 +86,17 @@ Description-Content-Type: text/markdown
91
86
  - [The library file](#the-library-file)
92
87
  - [Developer setup](#developer-setup)
93
88
 
94
- ## Notes
89
+ ## Python Compatibility
95
90
 
96
- Python 3.7 is supported by Jukebox up to version 0.4.1.
91
+ Jukebox 1.0+ requires Python 3.11 or newer.
97
92
 
98
- Python 3.8 is supported by Jukebox up to version 0.5.4.
99
-
100
- The `ui` extension is only available for Python versions 3.10 and above.
93
+ | Python version | Compatible Jukebox versions | Notes |
94
+ |----------------|-----------------------------|-------|
95
+ | 3.7 | 0.4.0 0.4.1 | Legacy |
96
+ | 3.8 | 0.4.0 – 0.5.4 | Legacy |
97
+ | 3.9 – 3.10 | 0.4.0 – 0.9.0 (incl. 1.0.0.dev13) | Legacy |
98
+ | 3.11 – 3.12 | 0.4.0 – latest | Actively supported |
99
+ | 3.13 | 0.5.3 – latest | Actively supported (see installation notes) |
101
100
 
102
101
  ## Install
103
102
 
@@ -190,10 +189,9 @@ jukebox-admin settings show --effective
190
189
  To use the `api` and `ui` commands, additional packages are required. You can install the `package[extra]` syntax regardless of the package manager you use, for example:
191
190
 
192
191
  ```shell
193
- # Python 3.9+ required
194
192
  uv tool install gukebox[api]
195
193
 
196
- # Python 3.10+ required, ui includes the api extra
194
+ # ui includes the api extra
197
195
  uv tool install gukebox[ui]
198
196
  ```
199
197
 
@@ -33,13 +33,17 @@
33
33
  - [The library file](#the-library-file)
34
34
  - [Developer setup](#developer-setup)
35
35
 
36
- ## Notes
36
+ ## Python Compatibility
37
37
 
38
- Python 3.7 is supported by Jukebox up to version 0.4.1.
38
+ Jukebox 1.0+ requires Python 3.11 or newer.
39
39
 
40
- Python 3.8 is supported by Jukebox up to version 0.5.4.
41
-
42
- The `ui` extension is only available for Python versions 3.10 and above.
40
+ | Python version | Compatible Jukebox versions | Notes |
41
+ |----------------|-----------------------------|-------|
42
+ | 3.7 | 0.4.0 0.4.1 | Legacy |
43
+ | 3.8 | 0.4.0 – 0.5.4 | Legacy |
44
+ | 3.9 – 3.10 | 0.4.0 – 0.9.0 (incl. 1.0.0.dev13) | Legacy |
45
+ | 3.11 – 3.12 | 0.4.0 – latest | Actively supported |
46
+ | 3.13 | 0.5.3 – latest | Actively supported (see installation notes) |
43
47
 
44
48
  ## Install
45
49
 
@@ -132,10 +136,9 @@ jukebox-admin settings show --effective
132
136
  To use the `api` and `ui` commands, additional packages are required. You can install the `package[extra]` syntax regardless of the package manager you use, for example:
133
137
 
134
138
  ```shell
135
- # Python 3.9+ required
136
139
  uv tool install gukebox[api]
137
140
 
138
- # Python 3.10+ required, ui includes the api extra
141
+ # ui includes the api extra
139
142
  uv tool install gukebox[ui]
140
143
  ```
141
144
 
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional
1
+ from typing import Any
2
2
 
3
3
  from fastapi import APIRouter, HTTPException, Response, status
4
4
  from pydantic import ValidationError
@@ -27,11 +27,11 @@ def build_current_tag_router(
27
27
  ) -> APIRouter:
28
28
  router = APIRouter(prefix="/api/v1", tags=["current-tag"])
29
29
 
30
- def read_current_tag_status() -> Optional[CurrentTagStatus]:
30
+ def read_current_tag_status() -> CurrentTagStatus | None:
31
31
  return get_current_tag_status.execute()
32
32
 
33
33
  def ensure_expected_tag_id_matches(
34
- expected_tag_id: Optional[str], current_tag_status: Optional[CurrentTagStatus]
34
+ expected_tag_id: str | None, current_tag_status: CurrentTagStatus | None
35
35
  ) -> None:
36
36
  if expected_tag_id is None:
37
37
  return
@@ -84,7 +84,7 @@ def build_current_tag_router(
84
84
  )
85
85
  def create_current_tag_disc(
86
86
  disc: DiscInput,
87
- expected_tag_id: Optional[str] = None,
87
+ expected_tag_id: str | None = None,
88
88
  ) -> Any:
89
89
  current_tag_status = read_current_tag_status()
90
90
  ensure_expected_tag_id_matches(expected_tag_id, current_tag_status)
@@ -112,7 +112,7 @@ def build_current_tag_router(
112
112
  )
113
113
  def update_current_tag_disc(
114
114
  disc_patch: DiscPatchInput,
115
- expected_tag_id: Optional[str] = None,
115
+ expected_tag_id: str | None = None,
116
116
  ) -> Any:
117
117
  current_tag_status = read_current_tag_status()
118
118
  ensure_expected_tag_id_matches(expected_tag_id, current_tag_status)
@@ -147,7 +147,7 @@ def build_current_tag_router(
147
147
  },
148
148
  summary="Delete the current tag disc",
149
149
  )
150
- def delete_current_tag_disc(expected_tag_id: Optional[str] = None) -> Response:
150
+ def delete_current_tag_disc(expected_tag_id: str | None = None) -> Response:
151
151
  current_tag_status = read_current_tag_status()
152
152
  ensure_expected_tag_id_matches(expected_tag_id, current_tag_status)
153
153
  if current_tag_status is None:
@@ -1,5 +1,3 @@
1
- from typing import Dict
2
-
3
1
  from fastapi import APIRouter, HTTPException, Response, status
4
2
  from pydantic import ValidationError
5
3
 
@@ -21,8 +19,8 @@ def build_discs_router(
21
19
  ) -> APIRouter:
22
20
  router = APIRouter(prefix="/api/v1", tags=["discs"])
23
21
 
24
- @router.get("/discs", response_model=Dict[str, DiscOutput], summary="List discs")
25
- def list_discs_route() -> Dict[str, Disc]:
22
+ @router.get("/discs", response_model=dict[str, DiscOutput], summary="List discs")
23
+ def list_discs_route() -> dict[str, Disc]:
26
24
  return list_discs.execute()
27
25
 
28
26
  @router.get("/discs/{tag_id}", response_model=DiscOutput, summary="Get a disc")
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, Optional
1
+ from typing import Any
2
2
 
3
3
  from pydantic import BaseModel, RootModel
4
4
 
@@ -14,21 +14,21 @@ class DiscOutput(Disc):
14
14
 
15
15
 
16
16
  class DiscPatchMetadataInput(BaseModel):
17
- artist: Optional[str] = None
18
- album: Optional[str] = None
19
- track: Optional[str] = None
20
- playlist: Optional[str] = None
17
+ artist: str | None = None
18
+ album: str | None = None
19
+ track: str | None = None
20
+ playlist: str | None = None
21
21
 
22
22
 
23
23
  class DiscPatchOptionInput(BaseModel):
24
- shuffle: Optional[bool] = None
25
- is_test: Optional[bool] = None
24
+ shuffle: bool | None = None
25
+ is_test: bool | None = None
26
26
 
27
27
 
28
28
  class DiscPatchInput(BaseModel):
29
- uri: Optional[str] = None
30
- metadata: Optional[DiscPatchMetadataInput] = None
31
- option: Optional[DiscPatchOptionInput] = None
29
+ uri: str | None = None
30
+ metadata: DiscPatchMetadataInput | None = None
31
+ option: DiscPatchOptionInput | None = None
32
32
 
33
33
 
34
34
  class CurrentTagStatusOutput(CurrentTagStatus):
@@ -44,5 +44,5 @@ class SettingsResetInput(BaseModel):
44
44
  path: str
45
45
 
46
46
 
47
- class SettingsPatchInput(RootModel[Dict[str, Any]]):
47
+ class SettingsPatchInput(RootModel[dict[str, Any]]):
48
48
  pass
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, cast
1
+ from typing import Any, cast
2
2
 
3
3
  from fastapi import APIRouter, HTTPException
4
4
 
@@ -11,21 +11,21 @@ from jukebox.settings.types import JsonObject
11
11
  def build_settings_router(settings_service: SettingsService) -> APIRouter:
12
12
  router = APIRouter(prefix="/api/v1", tags=["settings"])
13
13
 
14
- @router.get("/settings", response_model=Dict[str, Any], summary="Get persisted settings")
14
+ @router.get("/settings", response_model=dict[str, Any], summary="Get persisted settings")
15
15
  def get_settings() -> JsonObject:
16
16
  try:
17
17
  return settings_service.get_persisted_settings_view()
18
18
  except Exception as err:
19
19
  raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
20
20
 
21
- @router.get("/settings/effective", response_model=Dict[str, Any], summary="Get effective settings")
21
+ @router.get("/settings/effective", response_model=dict[str, Any], summary="Get effective settings")
22
22
  def get_effective_settings() -> JsonObject:
23
23
  try:
24
24
  return settings_service.get_effective_settings_view()
25
25
  except Exception as err:
26
26
  raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
27
27
 
28
- @router.patch("/settings", response_model=Dict[str, Any], summary="Patch persisted settings")
28
+ @router.patch("/settings", response_model=dict[str, Any], summary="Patch persisted settings")
29
29
  def patch_settings(patch: SettingsPatchInput) -> JsonObject:
30
30
  try:
31
31
  return settings_service.patch_persisted_settings(cast(JsonObject, patch.root))
@@ -34,7 +34,7 @@ def build_settings_router(settings_service: SettingsService) -> APIRouter:
34
34
  except Exception as err:
35
35
  raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
36
36
 
37
- @router.post("/settings/reset", response_model=Dict[str, Any], summary="Reset a persisted setting")
37
+ @router.post("/settings/reset", response_model=dict[str, Any], summary="Reset a persisted setting")
38
38
  def reset_settings(payload: SettingsResetInput) -> JsonObject:
39
39
  try:
40
40
  return settings_service.reset_persisted_value(payload.path)
@@ -1,5 +1,3 @@
1
- from typing import Optional
2
-
3
1
  from pydantic import BaseModel
4
2
 
5
3
  from jukebox.shared.dependency_messages import optional_extra_dependency_message
@@ -62,7 +60,7 @@ class SelectedSonosGroupOutput(SelectedSonosGroupSettings):
62
60
  class SonosSelectionMemberAvailabilityOutput(BaseModel):
63
61
  uid: str
64
62
  status: str
65
- speaker: Optional[SonosSpeakerOutput] = None
63
+ speaker: SonosSpeakerOutput | None = None
66
64
 
67
65
 
68
66
  class SonosSelectionAvailabilityOutput(BaseModel):
@@ -71,13 +69,13 @@ class SonosSelectionAvailabilityOutput(BaseModel):
71
69
 
72
70
 
73
71
  class SonosSelectionOutput(BaseModel):
74
- selected_group: Optional[SelectedSonosGroupOutput] = None
72
+ selected_group: SelectedSonosGroupOutput | None = None
75
73
  availability: SonosSelectionAvailabilityOutput
76
74
 
77
75
 
78
76
  class SonosSelectionInput(BaseModel):
79
77
  uids: list[str]
80
- coordinator_uid: Optional[str] = None
78
+ coordinator_uid: str | None = None
81
79
 
82
80
 
83
81
  class SonosSelectionUpdateOutput(BaseModel):
@@ -1,5 +1,4 @@
1
1
  import logging
2
- from typing import Union
3
2
 
4
3
  from discstore.adapters.inbound.cli_display import display_library_line, display_library_table
5
4
  from discstore.commands import (
@@ -43,24 +42,23 @@ class CLIController:
43
42
 
44
43
  def run(
45
44
  self,
46
- command: Union[
47
- CliAddCommand, CliListCommand, CliRemoveCommand, CliEditCommand, CliGetCommand, CliSearchCommand
48
- ],
45
+ command: CliAddCommand | CliListCommand | CliRemoveCommand | CliEditCommand | CliGetCommand | CliSearchCommand,
49
46
  ) -> None:
50
- if isinstance(command, CliAddCommand):
51
- self.add_disc_flow(command)
52
- elif isinstance(command, CliListCommand):
53
- self.list_discs_flow(command)
54
- elif isinstance(command, CliRemoveCommand):
55
- self.remove_disc_flow(command)
56
- elif isinstance(command, CliEditCommand):
57
- self.edit_disc_flow(command)
58
- elif isinstance(command, CliGetCommand):
59
- self.get_disc_flow(command)
60
- elif isinstance(command, CliSearchCommand):
61
- self.search_discs_flow(command)
62
- else:
63
- LOGGER.error(f"Command not implemented yet: command='{command}'")
47
+ match command:
48
+ case CliAddCommand():
49
+ self.add_disc_flow(command)
50
+ case CliListCommand():
51
+ self.list_discs_flow(command)
52
+ case CliRemoveCommand():
53
+ self.remove_disc_flow(command)
54
+ case CliEditCommand():
55
+ self.edit_disc_flow(command)
56
+ case CliGetCommand():
57
+ self.get_disc_flow(command)
58
+ case CliSearchCommand():
59
+ self.search_discs_flow(command)
60
+ case _:
61
+ LOGGER.error("Command not implemented yet: command='%s'", command)
64
62
 
65
63
  def add_disc_flow(self, command: CliAddCommand) -> None:
66
64
  tag = self.resolve_tag_id.execute(command.tag, command.use_current_tag)
@@ -79,7 +77,7 @@ class CLIController:
79
77
  if command.mode == "line":
80
78
  display_library_line(discs)
81
79
  return
82
- LOGGER.error(f"Displaying mode not implemented yet: mode='{command.mode}'")
80
+ LOGGER.error("Displaying mode not implemented yet: mode='%s'", command.mode)
83
81
 
84
82
  def remove_disc_flow(self, command: CliRemoveCommand) -> None:
85
83
  tag = self.resolve_tag_id.execute(command.tag, command.use_current_tag)
@@ -122,7 +120,7 @@ class CLIController:
122
120
  def search_discs_flow(self, command: CliSearchCommand) -> None:
123
121
  results = self.search_discs.execute(command.query)
124
122
  if not results:
125
- LOGGER.info(f"No discs found matching '{command.query}'")
123
+ LOGGER.info("No discs found matching '%s'", command.query)
126
124
  return
127
- LOGGER.info(f"Found {len(results)} disc(s) matching '{command.query}':")
125
+ LOGGER.info("Found %d disc(s) matching '%s':", len(results), command.query)
128
126
  display_library_table(results)
@@ -1,11 +1,9 @@
1
- from typing import Dict
2
-
3
1
  from discstore.domain.entities import Disc
4
2
 
5
3
  MAX_COL_WIDTH = 20
6
4
 
7
5
 
8
- def display_library_line(discs: Dict[str, Disc]) -> None:
6
+ def display_library_line(discs: dict[str, Disc]) -> None:
9
7
  if not discs:
10
8
  print("The library is empty")
11
9
  return
@@ -28,7 +26,7 @@ def truncate(text: str, max_length: int) -> str:
28
26
  return text[: max_length - 3] + "..."
29
27
 
30
28
 
31
- def display_library_table(discs: Dict[str, Disc]) -> None:
29
+ def display_library_table(discs: dict[str, Disc]) -> None:
32
30
  if not discs:
33
31
  print("The library is empty")
34
32
  return
@@ -1,5 +1,4 @@
1
1
  import logging
2
- from typing import Optional
3
2
 
4
3
  from discstore.adapters.inbound.cli_display import display_library_line, display_library_table
5
4
  from discstore.domain.entities import CurrentTagStatus, Disc, DiscMetadata, DiscOption
@@ -38,24 +37,25 @@ class InteractiveCLIController:
38
37
 
39
38
  def handle_command(self, command: str) -> None:
40
39
  try:
41
- if command == "add":
42
- self.add_disc_flow()
43
- elif command == "remove":
44
- self.remove_disc_flow()
45
- elif command == "list":
46
- self.list_discs_flow()
47
- elif command == "edit":
48
- self.edit_disc_flow()
49
- elif command == "current":
50
- self.current_tag_flow()
51
- elif command == "exit":
52
- print("See you soon!")
53
- exit(0)
54
- elif command == "help":
55
- print(self.help_message)
56
- else:
57
- print(f"Invalid command `{command}`")
58
- print(self.help_message)
40
+ match command:
41
+ case "add":
42
+ self.add_disc_flow()
43
+ case "remove":
44
+ self.remove_disc_flow()
45
+ case "list":
46
+ self.list_discs_flow()
47
+ case "edit":
48
+ self.edit_disc_flow()
49
+ case "current":
50
+ self.current_tag_flow()
51
+ case "exit":
52
+ print("See you soon!")
53
+ exit(0)
54
+ case "help":
55
+ print(self.help_message)
56
+ case _:
57
+ print(f"Invalid command `{command}`")
58
+ print(self.help_message)
59
59
  except Exception as err:
60
60
  print(f"Error: {err}")
61
61
  LOGGER.error("Error during handling command: %s", err)
@@ -112,7 +112,7 @@ class InteractiveCLIController:
112
112
  print(f"Tag ID : {current_tag_status.tag_id}")
113
113
  print(f"Known in library : {'yes' if current_tag_status.known_in_library else 'no'}")
114
114
 
115
- def _prompt_for_tag(self, current_tag_status: Optional[CurrentTagStatus], action: str) -> str:
115
+ def _prompt_for_tag(self, current_tag_status: CurrentTagStatus | None, action: str) -> str:
116
116
  default_tag = ""
117
117
  if current_tag_status is not None and (
118
118
  (action == "add" and not current_tag_status.known_in_library)