music-assistant: 2.3.6 -> 2.4.2 (#387392)

authored by Martin Weinelt and committed by GitHub 925ac765 274ea258

+229 -46
+5 -4
pkgs/by-name/mu/music-assistant/dont-install-deps.patch
··· 1 diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py 2 - index b6e5f2b4..0edead16 100644 3 --- a/music_assistant/helpers/util.py 4 +++ b/music_assistant/helpers/util.py 5 - @@ -424,29 +424,11 @@ async def load_provider_module(domain: str, requirements: list[str]) -> Provider 6 def _get_provider_module(domain: str) -> ProviderModuleType: 7 return importlib.import_module(f".{domain}", "music_assistant.providers") 8 ··· 29 - # try loading the provider again to be safe 30 - # this will fail if something else is wrong (as it should) 31 - return await asyncio.to_thread(_get_provider_module, domain) 32 - + raise RuntimeError(f"Missing dependencies for provider {domain}") 33 - 34 35 def create_tempfile():
··· 1 diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py 2 + index 8daf159d..af5a6f38 100644 3 --- a/music_assistant/helpers/util.py 4 +++ b/music_assistant/helpers/util.py 5 + @@ -429,30 +429,11 @@ async def load_provider_module(domain: str, requirements: list[str]) -> Provider 6 def _get_provider_module(domain: str) -> ProviderModuleType: 7 return importlib.import_module(f".{domain}", "music_assistant.providers") 8 ··· 29 - # try loading the provider again to be safe 30 - # this will fail if something else is wrong (as it should) 31 - return await asyncio.to_thread(_get_provider_module, domain) 32 + - 33 + + raise RuntimeError(f"Missing dependencies for provider {domain}.") 34 35 def create_tempfile(): 36 + """Return a (named) temporary file."""
+19 -19
pkgs/by-name/mu/music-assistant/ffmpeg.patch
··· 1 diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py 2 - index dad3c5db..d1398d66 100644 3 --- a/music_assistant/helpers/audio.py 4 +++ b/music_assistant/helpers/audio.py 5 - @@ -74,7 +74,7 @@ async def crossfade_pcm_parts( 6 await outfile.write(fade_out_part) 7 args = [ 8 # generic args ··· 11 "-hide_banner", 12 "-loglevel", 13 "quiet", 14 - @@ -135,7 +135,7 @@ async def strip_silence( 15 reverse: bool = False, 16 ) -> bytes: 17 """Strip silence from begin or end of pcm audio using ffmpeg.""" ··· 20 args += [ 21 "-acodec", 22 pcm_format.content_type.name.lower(), 23 - @@ -734,7 +734,7 @@ async def get_file_stream( 24 - async def check_audio_support() -> tuple[bool, bool, str]: 25 - """Check if ffmpeg is present (with/without libsoxr support).""" 26 - # check for FFmpeg presence 27 - - returncode, output = await check_output("ffmpeg", "-version") 28 - + returncode, output = await check_output("@ffmpeg@", "-version") 29 - ffmpeg_present = returncode == 0 and "FFmpeg" in output.decode() 30 - 31 - # use globals as in-memory cache 32 - @@ -789,7 +789,7 @@ async def get_silence( 33 return 34 # use ffmpeg for all other encodings 35 args = [ ··· 39 "-loglevel", 40 "quiet", 41 diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py 42 - index 0405fc27..570f9157 100644 43 --- a/music_assistant/helpers/ffmpeg.py 44 +++ b/music_assistant/helpers/ffmpeg.py 45 - @@ -213,7 +213,7 @@ def get_ffmpeg_args( # noqa: PLR0915 46 - 47 # generic args 48 generic_args = [ 49 - "ffmpeg", ··· 51 "-hide_banner", 52 "-loglevel", 53 loglevel, 54 diff --git a/music_assistant/helpers/tags.py b/music_assistant/helpers/tags.py 55 - index 55def74b..7c26e13d 100644 56 --- a/music_assistant/helpers/tags.py 57 +++ b/music_assistant/helpers/tags.py 58 - @@ -387,7 +387,7 @@ async def parse_tags(input_file: str, file_size: int | None = None) -> AudioTags 59 Input_file may be a (local) filename or URL accessible by ffmpeg. 60 """ 61 args = ( ··· 64 "-hide_banner", 65 "-loglevel", 66 "fatal", 67 - @@ -448,7 +448,7 @@ async def get_embedded_image(input_file: str) -> bytes | None: 68 Input_file may be a (local) filename or URL accessible by ffmpeg. 69 """ 70 args = (
··· 1 diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py 2 + index 482ab8e8..a02fc435 100644 3 --- a/music_assistant/helpers/audio.py 4 +++ b/music_assistant/helpers/audio.py 5 + @@ -80,7 +80,7 @@ async def crossfade_pcm_parts( 6 await outfile.write(fade_out_part) 7 args = [ 8 # generic args ··· 11 "-hide_banner", 12 "-loglevel", 13 "quiet", 14 + @@ -141,7 +141,7 @@ async def strip_silence( 15 reverse: bool = False, 16 ) -> bytes: 17 """Strip silence from begin or end of pcm audio using ffmpeg.""" ··· 20 args += [ 21 "-acodec", 22 pcm_format.content_type.name.lower(), 23 + @@ -912,7 +912,7 @@ async def get_silence( 24 return 25 # use ffmpeg for all other encodings 26 args = [ ··· 30 "-loglevel", 31 "quiet", 32 diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py 33 + index 7c1c8d83..501a7370 100644 34 --- a/music_assistant/helpers/ffmpeg.py 35 +++ b/music_assistant/helpers/ffmpeg.py 36 + @@ -218,7 +218,7 @@ def get_ffmpeg_args( 37 + extra_args = [] 38 # generic args 39 generic_args = [ 40 - "ffmpeg", ··· 42 "-hide_banner", 43 "-loglevel", 44 loglevel, 45 + @@ -370,7 +370,7 @@ async def check_ffmpeg_version() -> None: 46 + """Check if ffmpeg is present (with libsoxr support).""" 47 + # check for FFmpeg presence 48 + try: 49 + - returncode, output = await check_output("ffmpeg", "-version") 50 + + returncode, output = await check_output("@ffmpeg@", "-version") 51 + except FileNotFoundError: 52 + raise AudioError( 53 + "FFmpeg binary is missing from system. " 54 diff --git a/music_assistant/helpers/tags.py b/music_assistant/helpers/tags.py 55 + index 06c8bcb5..a703aacd 100644 56 --- a/music_assistant/helpers/tags.py 57 +++ b/music_assistant/helpers/tags.py 58 + @@ -438,7 +438,7 @@ def parse_tags(input_file: str, file_size: int | None = None) -> AudioTags: 59 Input_file may be a (local) filename or URL accessible by ffmpeg. 60 """ 61 args = ( ··· 64 "-hide_banner", 65 "-loglevel", 66 "fatal", 67 + @@ -553,7 +553,7 @@ async def get_embedded_image(input_file: str) -> bytes | None: 68 Input_file may be a (local) filename or URL accessible by ffmpeg. 69 """ 70 args = (
+2 -2
pkgs/by-name/mu/music-assistant/frontend.nix
··· 7 8 buildPythonPackage rec { 9 pname = "music-assistant-frontend"; 10 - version = "2.9.16"; 11 pyproject = true; 12 13 src = fetchPypi { 14 inherit pname version; 15 - hash = "sha256-bUtclj8GMq1JdC4tYNETl+l+TNF91y4yTANSpuOOm70="; 16 }; 17 18 postPatch = ''
··· 7 8 buildPythonPackage rec { 9 pname = "music-assistant-frontend"; 10 + version = "2.11.11"; 11 pyproject = true; 12 13 src = fetchPypi { 14 inherit pname version; 15 + hash = "sha256-fPFszZfJjdc2KTQipeb2KVbIwWwL56bqFGba1LSJXuQ="; 16 }; 17 18 postPatch = ''
+29
pkgs/by-name/mu/music-assistant/librespot.patch
···
··· 1 + diff --git a/music_assistant/providers/spotify/helpers.py b/music_assistant/providers/spotify/helpers.py 2 + index 8b6c4e78..20c2a269 100644 3 + --- a/music_assistant/providers/spotify/helpers.py 4 + +++ b/music_assistant/providers/spotify/helpers.py 5 + @@ -11,23 +11,4 @@ from music_assistant.helpers.process import check_output 6 + async def get_librespot_binary() -> str: 7 + """Find the correct librespot binary belonging to the platform.""" 8 + 9 + - # ruff: noqa: SIM102 10 + - async def check_librespot(librespot_path: str) -> str | None: 11 + - try: 12 + - returncode, output = await check_output(librespot_path, "--version") 13 + - if returncode == 0 and b"librespot" in output: 14 + - return librespot_path 15 + - except OSError: 16 + - return None 17 + - 18 + - base_path = os.path.join(os.path.dirname(__file__), "bin") 19 + - system = platform.system().lower().replace("darwin", "macos") 20 + - architecture = platform.machine().lower() 21 + - 22 + - if bridge_binary := await check_librespot( 23 + - os.path.join(base_path, f"librespot-{system}-{architecture}") 24 + - ): 25 + - return bridge_binary 26 + - 27 + - msg = f"Unable to locate Librespot for {system}/{architecture}" 28 + - raise RuntimeError(msg) 29 + + return "@librespot@"
+16 -17
pkgs/by-name/mu/music-assistant/package.nix
··· 3 python3, 4 fetchFromGitHub, 5 ffmpeg-headless, 6 nixosTests, 7 replaceVars, 8 providers ? [ ], ··· 12 python = python3.override { 13 self = python; 14 packageOverrides = self: super: { 15 - aiojellyfin = super.aiojellyfin.overridePythonAttrs (oldAttrs: rec { 16 - version = "0.10.1"; 17 - 18 - src = fetchFromGitHub { 19 - owner = "Jc2k"; 20 - repo = "aiojellyfin"; 21 - tag = "v${version}"; 22 - hash = "sha256-A+uvM1/7HntRMIdknfHr0TMGIjHk7BCwsZopXdVoEO8="; 23 - }; 24 - }); 25 - 26 music-assistant-frontend = self.callPackage ./frontend.nix { }; 27 28 music-assistant-models = super.music-assistant-models.overridePythonAttrs (oldAttrs: rec { 29 - version = "1.1.4"; 30 31 src = fetchFromGitHub { 32 owner = "music-assistant"; 33 repo = "models"; 34 tag = version; 35 - hash = "sha256-keig18o32X53q/QcoaPO0o9AT4XTEZ+dQ3L6u6BVkLU="; 36 }; 37 38 postPatch = '' ··· 54 55 python.pkgs.buildPythonApplication rec { 56 pname = "music-assistant"; 57 - version = "2.3.6"; 58 pyproject = true; 59 60 src = fetchFromGitHub { 61 owner = "music-assistant"; 62 repo = "server"; 63 tag = version; 64 - hash = "sha256-CSGpG1E4ou1TGz/S1mXFHyk49p7dStEwxUTB+xxfNEc="; 65 }; 66 67 patches = [ ··· 69 ffmpeg = "${lib.getBin ffmpeg-headless}/bin/ffmpeg"; 70 ffprobe = "${lib.getBin ffmpeg-headless}/bin/ffprobe"; 71 }) 72 73 # Disable interactive dependency resolution, which clashes with the immutable Python environment 74 ./dont-install-deps.patch ··· 77 postPatch = '' 78 substituteInPlace pyproject.toml \ 79 --replace-fail "0.0.0" "${version}" 80 ''; 81 82 build-system = with python.pkgs; [ ··· 85 86 pythonRelaxDeps = [ 87 "aiohttp" 88 "certifi" 89 "colorlog" 90 "cryptography" ··· 123 memory-tempfile 124 music-assistant-frontend 125 music-assistant-models 126 orjson 127 pillow 128 python-slugify 129 shortuuid 130 unidecode ··· 136 nativeCheckInputs = 137 with python.pkgs; 138 [ 139 - aiojellyfin 140 pytest-aiohttp 141 pytest-cov-stub 142 pytest-timeout ··· 144 syrupy 145 pytest-timeout 146 ] 147 - ++ lib.flatten (lib.attrValues optional-dependencies); 148 149 pytestFlagsArray = [ 150 # blocks in poll()
··· 3 python3, 4 fetchFromGitHub, 5 ffmpeg-headless, 6 + librespot, 7 nixosTests, 8 replaceVars, 9 providers ? [ ], ··· 13 python = python3.override { 14 self = python; 15 packageOverrides = self: super: { 16 music-assistant-frontend = self.callPackage ./frontend.nix { }; 17 18 music-assistant-models = super.music-assistant-models.overridePythonAttrs (oldAttrs: rec { 19 + version = "1.1.30"; 20 21 src = fetchFromGitHub { 22 owner = "music-assistant"; 23 repo = "models"; 24 tag = version; 25 + hash = "sha256-ZLTRHarjVFAk+tYPkgLm192rE+C82vNzqs8PmJhGSeg="; 26 }; 27 28 postPatch = '' ··· 44 45 python.pkgs.buildPythonApplication rec { 46 pname = "music-assistant"; 47 + version = "2.4.2"; 48 pyproject = true; 49 50 src = fetchFromGitHub { 51 owner = "music-assistant"; 52 repo = "server"; 53 tag = version; 54 + hash = "sha256-5FIIXIn4tEz6w/uAh6PGkU4tU+mz7Jpb3+bq1mRNr2Y="; 55 }; 56 57 patches = [ ··· 59 ffmpeg = "${lib.getBin ffmpeg-headless}/bin/ffmpeg"; 60 ffprobe = "${lib.getBin ffmpeg-headless}/bin/ffprobe"; 61 }) 62 + (replaceVars ./librespot.patch { 63 + librespot = lib.getExe librespot; 64 + }) 65 66 # Disable interactive dependency resolution, which clashes with the immutable Python environment 67 ./dont-install-deps.patch ··· 70 postPatch = '' 71 substituteInPlace pyproject.toml \ 72 --replace-fail "0.0.0" "${version}" 73 + 74 + rm -rv music_assistant/providers/spotify/bin 75 ''; 76 77 build-system = with python.pkgs; [ ··· 80 81 pythonRelaxDeps = [ 82 "aiohttp" 83 + "aiosqlite" 84 "certifi" 85 "colorlog" 86 "cryptography" ··· 119 memory-tempfile 120 music-assistant-frontend 121 music-assistant-models 122 + mutagen 123 orjson 124 pillow 125 + podcastparser 126 python-slugify 127 shortuuid 128 unidecode ··· 134 nativeCheckInputs = 135 with python.pkgs; 136 [ 137 pytest-aiohttp 138 pytest-cov-stub 139 pytest-timeout ··· 141 syrupy 142 pytest-timeout 143 ] 144 + ++ lib.flatten (lib.attrValues optional-dependencies) 145 + ++ (providerPackages.jellyfin python.pkgs) 146 + ++ (providerPackages.opensubsonic python.pkgs); 147 148 pytestFlagsArray = [ 149 # blocks in poll()
+20 -2
pkgs/by-name/mu/music-assistant/providers.nix
··· 1 # Do not edit manually, run ./update-providers.py 2 3 { 4 - version = "2.3.6"; 5 providers = { 6 airplay = ps: [ 7 ]; 8 apple_music = ps: [ 9 ]; # missing pywidevine 10 bluesound = 11 ps: with ps; [ 12 pyblu ··· 19 ]; 20 deezer = 21 ps: with ps; [ 22 pycryptodome 23 - ]; # missing deezer-python-async 24 dlna = 25 ps: with ps; [ 26 async-upnp-client ··· 41 ]; 42 hass_players = ps: [ 43 ]; 44 jellyfin = 45 ps: with ps; [ 46 aiojellyfin ··· 57 ps: with ps; [ 58 plexapi 59 ]; 60 qobuz = ps: [ 61 ]; 62 radiobrowser = ··· 87 ps: with ps; [ 88 pkce 89 ]; 90 template_player_provider = ps: [ 91 ]; 92 test = ps: [ ··· 101 ]; 102 ytmusic = 103 ps: with ps; [ 104 yt-dlp 105 ytmusicapi 106 ];
··· 1 # Do not edit manually, run ./update-providers.py 2 3 { 4 + version = "2.4.2"; 5 providers = { 6 airplay = ps: [ 7 ]; 8 apple_music = ps: [ 9 ]; # missing pywidevine 10 + audible = 11 + ps: with ps; [ 12 + audible 13 + ]; 14 + audiobookshelf = 15 + ps: with ps; [ 16 + aioaudiobookshelf 17 + ]; 18 bluesound = 19 ps: with ps; [ 20 pyblu ··· 27 ]; 28 deezer = 29 ps: with ps; [ 30 + deezer-python-async 31 pycryptodome 32 + ]; 33 dlna = 34 ps: with ps; [ 35 async-upnp-client ··· 50 ]; 51 hass_players = ps: [ 52 ]; 53 + ibroadcast = ps: [ 54 + ]; # missing ibroadcastaio 55 jellyfin = 56 ps: with ps; [ 57 aiojellyfin ··· 68 ps: with ps; [ 69 plexapi 70 ]; 71 + podcastfeed = 72 + ps: with ps; [ 73 + podcastparser 74 + ]; 75 qobuz = ps: [ 76 ]; 77 radiobrowser = ··· 102 ps: with ps; [ 103 pkce 104 ]; 105 + spotify_connect = ps: [ 106 + ]; 107 template_player_provider = ps: [ 108 ]; 109 test = ps: [ ··· 118 ]; 119 ytmusic = 120 ps: with ps; [ 121 + duration-parser 122 yt-dlp 123 ytmusicapi 124 ];
+43
pkgs/development/python-modules/aioaudiobookshelf/default.nix
···
··· 1 + { 2 + lib, 3 + buildPythonPackage, 4 + fetchFromGitHub, 5 + setuptools, 6 + aiohttp, 7 + mashumaro, 8 + python-socketio, 9 + }: 10 + 11 + buildPythonPackage rec { 12 + pname = "aioaudiobookshelf"; 13 + version = "0.1.4"; 14 + pyproject = true; 15 + 16 + src = fetchFromGitHub { 17 + owner = "music-assistant"; 18 + repo = "aioaudiobookshelf"; 19 + tag = version; 20 + hash = "sha256-rUd8TwmQDeMxXu7/UBnN426N+BMas2u6RNpTBXa3/5g="; 21 + }; 22 + 23 + build-system = [ 24 + setuptools 25 + ]; 26 + 27 + dependencies = [ 28 + aiohttp 29 + mashumaro 30 + python-socketio 31 + ]; 32 + 33 + pythonImportsCheck = [ 34 + "aioaudiobookshelf" 35 + ]; 36 + 37 + meta = { 38 + description = "Async python library to interact with Audiobookshelf"; 39 + homepage = "https://github.com/music-assistant/aioaudiobookshelf"; 40 + license = lib.licenses.asl20; 41 + maintainers = with lib.maintainers; [ hexa ]; 42 + }; 43 + }
+2 -2
pkgs/development/python-modules/aiorun/default.nix
··· 11 12 buildPythonPackage rec { 13 pname = "aiorun"; 14 - version = "2024.8.1"; 15 pyproject = true; 16 17 disabled = pythonOlder "3.7"; ··· 20 owner = "cjrh"; 21 repo = "aiorun"; 22 tag = "v${version}"; 23 - hash = "sha256-D+4IceCdPg1Akq1YgPuSGF7yAOhDe8PmioNBE5X7yuQ="; 24 }; 25 26 build-system = [ flit-core ];
··· 11 12 buildPythonPackage rec { 13 pname = "aiorun"; 14 + version = "2025.1.1"; 15 pyproject = true; 16 17 disabled = pythonOlder "3.7"; ··· 20 owner = "cjrh"; 21 repo = "aiorun"; 22 tag = "v${version}"; 23 + hash = "sha256-YqUlWf79EbC47BETBDjo8hzg5jhL4LiWLKGr1Qy4AbM="; 24 }; 25 26 build-system = [ flit-core ];
+48
pkgs/development/python-modules/deezer-python-async/default.nix
···
··· 1 + { 2 + lib, 3 + buildPythonPackage, 4 + fetchFromGitHub, 5 + 6 + # build-system 7 + poetry-core, 8 + 9 + # dependencies 10 + aiohttp, 11 + asyncio-throttle, 12 + }: 13 + 14 + buildPythonPackage rec { 15 + pname = "deezer-python-async"; 16 + version = "0.3.0"; 17 + pyproject = true; 18 + 19 + src = fetchFromGitHub { 20 + owner = "music-assistant"; 21 + repo = "deezer-python-async"; 22 + tag = "v${version}"; 23 + hash = "sha256-uuAB+SC/ECG50ox/6Bi+94bAt+YZokeQChpDQUAK+zc="; 24 + }; 25 + 26 + build-system = [ 27 + poetry-core 28 + ]; 29 + 30 + dependencies = [ 31 + aiohttp 32 + asyncio-throttle 33 + ]; 34 + 35 + doCheck = false; # requires access to the deezer api 36 + 37 + pythonImportsCheck = [ 38 + "deezer" 39 + ]; 40 + 41 + meta = { 42 + description = "Deezer client for python *but async"; 43 + homepage = "https://github.com/music-assistant/deezer-python-async"; 44 + changelog = "https://github.com/music-assistant/deezer-python-async/blob/${src.tag}/CHANGELOG.md"; 45 + license = lib.licenses.mit; 46 + maintainers = with lib.maintainers; [ hexa ]; 47 + }; 48 + }
+39
pkgs/development/python-modules/duration-parser/default.nix
···
··· 1 + { 2 + lib, 3 + buildPythonPackage, 4 + fetchFromGitHub, 5 + setuptools, 6 + pytestCheckHook, 7 + }: 8 + 9 + buildPythonPackage rec { 10 + pname = "duration-parser"; 11 + version = "1.0.1"; 12 + pyproject = true; 13 + 14 + src = fetchFromGitHub { 15 + owner = "adriansahlman"; 16 + repo = "duration-parser"; 17 + tag = "v${version}"; 18 + hash = "sha256-Vn3H2JEMrJ6b/7eNG+h9tG5QzslGvaV3sunM7UO9Bok="; 19 + }; 20 + 21 + build-system = [ 22 + setuptools 23 + ]; 24 + 25 + nativeCheckInputs = [ 26 + pytestCheckHook 27 + ]; 28 + 29 + pythonImportsCheck = [ 30 + "duration_parser" 31 + ]; 32 + 33 + meta = { 34 + description = "A minimal duration parser written in python"; 35 + homepage = "https://github.com/adriansahlman/duration-parser"; 36 + license = lib.licenses.mit; 37 + maintainers = with lib.maintainers; [ hexa ]; 38 + }; 39 + }
+6
pkgs/top-level/python-packages.nix
··· 185 186 aioasuswrt = callPackage ../development/python-modules/aioasuswrt { }; 187 188 aioautomower = callPackage ../development/python-modules/aioautomower { }; 189 190 aioazuredevops = callPackage ../development/python-modules/aioazuredevops { }; ··· 3194 3195 deezer-python = callPackage ../development/python-modules/deezer-python { }; 3196 3197 defang = callPackage ../development/python-modules/defang { }; 3198 3199 defcon = callPackage ../development/python-modules/defcon { }; ··· 4035 dungeon-eos = callPackage ../development/python-modules/dungeon-eos { }; 4036 4037 duo-client = callPackage ../development/python-modules/duo-client { }; 4038 4039 durationpy = callPackage ../development/python-modules/durationpy { }; 4040
··· 185 186 aioasuswrt = callPackage ../development/python-modules/aioasuswrt { }; 187 188 + aioaudiobookshelf = callPackage ../development/python-modules/aioaudiobookshelf { }; 189 + 190 aioautomower = callPackage ../development/python-modules/aioautomower { }; 191 192 aioazuredevops = callPackage ../development/python-modules/aioazuredevops { }; ··· 3196 3197 deezer-python = callPackage ../development/python-modules/deezer-python { }; 3198 3199 + deezer-python-async = callPackage ../development/python-modules/deezer-python-async { }; 3200 + 3201 defang = callPackage ../development/python-modules/defang { }; 3202 3203 defcon = callPackage ../development/python-modules/defcon { }; ··· 4039 dungeon-eos = callPackage ../development/python-modules/dungeon-eos { }; 4040 4041 duo-client = callPackage ../development/python-modules/duo-client { }; 4042 + 4043 + duration-parser = callPackage ../development/python-modules/duration-parser { }; 4044 4045 durationpy = callPackage ../development/python-modules/durationpy { }; 4046