diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a8c640..1ed92fa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.11.5" - uses: Gr1N/setup-poetry@v8 - uses: actions/cache@v3 with: diff --git a/.gitignore b/.gitignore index 10e75fd..a849618 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST -poetry.lock # Code Editors .vscode diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..fb4b69d --- /dev/null +++ b/poetry.lock @@ -0,0 +1,804 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "0.7.13" +description = "A configurable sidebar-enabled Sphinx theme" +optional = false +python-versions = ">=3.6" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + +[[package]] +name = "babel" +version = "2.12.1" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, +] + +[[package]] +name = "black" +version = "23.7.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[[package]] +name = "click" +version = "8.1.6" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "docutils" +version = "0.20.1" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, +] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "3.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "responses" +version = "0.23.3" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.7" +files = [ + {file = "responses-0.23.3-py3-none-any.whl", hash = "sha256:e6fbcf5d82172fecc0aa1860fd91e58cbfd96cee5e96da5b63fa6eb3caa10dd3"}, + {file = "responses-0.23.3.tar.gz", hash = "sha256:205029e1cb334c21cb4ec64fc7599be48b859a0fd381a42443cdd600bfe8b16a"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +types-PyYAML = "*" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] + +[[package]] +name = "ruff" +version = "0.0.280" +description = "An extremely fast Python linter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.280-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:48ed5aca381050a4e2f6d232db912d2e4e98e61648b513c350990c351125aaec"}, + {file = "ruff-0.0.280-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ef6ee3e429fd29d6a5ceed295809e376e6ece5b0f13c7e703efaf3d3bcb30b96"}, + {file = "ruff-0.0.280-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d878370f7e9463ac40c253724229314ff6ebe4508cdb96cb536e1af4d5a9cd4f"}, + {file = "ruff-0.0.280-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83e8f372fa5627eeda5b83b5a9632d2f9c88fc6d78cead7e2a1f6fb05728d137"}, + {file = "ruff-0.0.280-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7008fc6ca1df18b21fa98bdcfc711dad5f94d0fc3c11791f65e460c48ef27c82"}, + {file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe7118c1eae3fda17ceb409629c7f3b5a22dffa7caf1f6796776936dca1fe653"}, + {file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37359cd67d2af8e09110a546507c302cbea11c66a52d2a9b6d841d465f9962d4"}, + {file = "ruff-0.0.280-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd58af46b0221efb95966f1f0f7576df711cb53e50d2fdb0e83c2f33360116a4"}, + {file = "ruff-0.0.280-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e7c15828d09f90e97bea8feefcd2907e8c8ce3a1f959c99f9b4b3469679f33c"}, + {file = "ruff-0.0.280-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2dae8f2d9c44c5c49af01733c2f7956f808db682a4193180dedb29dd718d7bbe"}, + {file = "ruff-0.0.280-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5f972567163a20fb8c2d6afc60c2ea5ef8b68d69505760a8bd0377de8984b4f6"}, + {file = "ruff-0.0.280-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ffa7347ad11643f29de100977c055e47c988cd6d9f5f5ff83027600b11b9189"}, + {file = "ruff-0.0.280-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37dab70114671d273f203268f6c3366c035fe0c8056614069e90a65e614bfc"}, + {file = "ruff-0.0.280-py3-none-win32.whl", hash = "sha256:7784e3606352fcfb193f3cd22b2e2117c444cb879ef6609ec69deabd662b0763"}, + {file = "ruff-0.0.280-py3-none-win_amd64.whl", hash = "sha256:4a7d52457b5dfcd3ab24b0b38eefaead8e2dca62b4fbf10de4cd0938cf20ce30"}, + {file = "ruff-0.0.280-py3-none-win_arm64.whl", hash = "sha256:b7de5b8689575918e130e4384ed9f539ce91d067c0a332aedef6ca7188adac2d"}, + {file = "ruff-0.0.280.tar.gz", hash = "sha256:581c43e4ac5e5a7117ad7da2120d960a4a99e68ec4021ec3cd47fe1cf78f8380"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "7.1.2" +description = "Python documentation generator" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, + {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, +] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.18.1,<0.21" +imagesize = ">=1.3" +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.13" +requests = ">=2.25.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] +test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, + {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, + {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "types-python-dateutil" +version = "2.8.19.14" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = "*" +files = [ + {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, + {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.11" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.11.tar.gz", hash = "sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b"}, + {file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"}, +] + +[[package]] +name = "types-requests" +version = "2.31.0.2" +description = "Typing stubs for requests" +optional = false +python-versions = "*" +files = [ + {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"}, + {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"}, +] + +[package.dependencies] +types-urllib3 = "*" + +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +description = "Typing stubs for urllib3" +optional = false +python-versions = "*" +files = [ + {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, + {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "urllib3" +version = "2.0.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, + {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "3cc7deae10b021ceb4c806d635b9b241a31c3683da2caa57135452c48332c960" diff --git a/src/knuckles/api.py b/src/knuckles/api.py index 8fb394c..e9b81e7 100644 --- a/src/knuckles/api.py +++ b/src/knuckles/api.py @@ -101,9 +101,7 @@ def generate_params(self, extra_params: dict[str, Any] = {}) -> dict[str, Any]: return {**params, "t": token, "s": salt} - def request( - self, endpoint: str, extra_params: dict[str, Any] = {} - ) -> dict[str, Any]: + def raw_request(self, endpoint: str, extra_params: dict[str, Any]) -> Response: """Make a request to the Subsonic API. :param endpoint: The endpoint where the request should be made, @@ -112,18 +110,38 @@ def request( :type endpoint: str :param extra_params: The extra parameters required by the endpoint, defaults to {}. - :type extra_params: dict[str, Any], optional :raises code_error: Raises an exception with the format CodeErrorXX or UnknownCodeError if the request fails. - :return: The JSON data inside the "subsonic-response" object. + :return: The Response object returned by the request. :rtype: dict[str, Any] """ - response: Response = requests.get( + return requests.get( url=f"{self.url}/rest/{endpoint}", params=self.generate_params(extra_params), ) + def json_request( + self, endpoint: str, extra_params: dict[str, Any] = {} + ) -> dict[str, Any]: + """Make a request to the Subsonic API and returns a JSON response. + Don't use with binary data endpoints. + + :param endpoint: The endpoint where the request should be made, + only specifies the route name, without slashes. + E.g. "ping", "getLicense", etc. + :type endpoint: str + :param extra_params: The extra parameters required by the endpoint, + defaults to {}. + :type extra_params: dict[str, Any], optional + :raises code_error: Raises an exception with the format CodeErrorXX or + UnknownCodeError if the request fails. + :return: The JSON data inside the "subsonic-response" object. + :rtype: dict[str, Any] + """ + + response = self.raw_request(endpoint, extra_params) + json_response: dict[str, Any] = response.json()["subsonic-response"] if json_response["status"] == "failed": diff --git a/src/knuckles/bookmarks.py b/src/knuckles/bookmarks.py index cdebde1..0c78144 100644 --- a/src/knuckles/bookmarks.py +++ b/src/knuckles/bookmarks.py @@ -25,7 +25,7 @@ def get_bookmarks(self) -> list[Bookmark]: :rtype: list[Bookmark] """ - response = self.api.request("getBookmarks")["bookmarks"]["bookmark"] + response = self.api.json_request("getBookmarks")["bookmarks"]["bookmark"] return [Bookmark(self.subsonic, **bookmark) for bookmark in response] @@ -62,7 +62,7 @@ def create_bookmark( :rtype: Bookmark """ - self.api.request( + self.api.json_request( "createBookmark", {"id": id, "position": position, "comment": comment} ) @@ -97,7 +97,7 @@ def delete_bookmark(self, id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("deleteBookmark", {"id": id}) + self.api.json_request("deleteBookmark", {"id": id}) return self.subsonic @@ -108,7 +108,7 @@ def get_play_queue(self) -> PlayQueue: :rtype: PlayQueue """ - response = self.api.request("getPlayQueue")["playQueue"] + response = self.api.json_request("getPlayQueue")["playQueue"] return PlayQueue(self.subsonic, **response) @@ -132,12 +132,11 @@ def save_play_queue( :rtype: PlayQueue """ - self.api.request( + self.api.json_request( "savePlayQueue", {"id": song_ids, "current": current_song_id, "position": position}, ) - # TODO This approach is expensive, a better one is preferred # Fake the song structure given by in the API. songs = [] for song_id in song_ids: diff --git a/src/knuckles/browsing.py b/src/knuckles/browsing.py index 5ab72b4..d3998b6 100644 --- a/src/knuckles/browsing.py +++ b/src/knuckles/browsing.py @@ -28,7 +28,9 @@ def get_music_folders(self) -> list[MusicFolder]: :rtype: list[MusicFolder] """ - response = self.api.request("getMusicFolders")["musicFolders"]["musicFolder"] + response = self.api.json_request("getMusicFolders")["musicFolders"][ + "musicFolder" + ] return [MusicFolder(self.subsonic, **music_folder) for music_folder in response] @@ -39,7 +41,7 @@ def get_genres(self) -> list[Genre]: :rtype: list[Genre] """ - response = self.api.request("getGenres")["genres"]["genre"] + response = self.api.json_request("getGenres")["genres"]["genre"] return [Genre(self.subsonic, **genre) for genre in response] @@ -49,7 +51,7 @@ def get_genre(self, name: str) -> Genre | None: :param name: The name of the genre to get. :type name: str :return: A genre object that correspond with the given name - or None if if no genre found. + or None if is no genre found. :rtype: Genre | None """ @@ -71,9 +73,9 @@ def get_artists(self, music_folder_id: str | None = None) -> list[Artist]: :rtype: list[Artist] """ - response = self.api.request("getArtists", {"musicFolderId": music_folder_id})[ - "artists" - ]["index"] + response = self.api.json_request( + "getArtists", {"musicFolderId": music_folder_id} + )["artists"]["index"] artists: list[Artist] = [] @@ -94,7 +96,7 @@ def get_artist(self, id: str) -> Artist: :rtype: Artist """ - response = self.api.request("getArtist", {"id": id})["artist"] + response = self.api.json_request("getArtist", {"id": id})["artist"] return Artist(self.subsonic, **response) @@ -108,7 +110,7 @@ def get_album(self, id: str) -> Album: :rtype: Album """ - response = self.api.request("getAlbum", {"id": id})["album"] + response = self.api.json_request("getAlbum", {"id": id})["album"] return Album(self.subsonic, **response) @@ -121,7 +123,7 @@ def get_album_info(self, id: str) -> AlbumInfo: :rtype: AlbumInfo """ - response = self.api.request("getAlbumInfo2", {"id": id})["albumInfo"] + response = self.api.json_request("getAlbumInfo2", {"id": id})["albumInfo"] return AlbumInfo(self.subsonic, id, **response) @@ -135,14 +137,14 @@ def get_song(self, id: str) -> Song: :rtype: Song """ - response = self.api.request("getSong", {"id": id})["song"] + response = self.api.json_request("getSong", {"id": id})["song"] return Song(self.subsonic, **response) def get_artist_info( self, id: str, count: int | None = None, include_not_present: bool | None = None ) -> ArtistInfo: - response = self.api.request( + response = self.api.json_request( "getArtistInfo2", {"id": id, "count": count, "includeNotPresent": include_not_present}, )["artistInfo2"] diff --git a/src/knuckles/chat.py b/src/knuckles/chat.py index a6f22ae..fb4c4aa 100644 --- a/src/knuckles/chat.py +++ b/src/knuckles/chat.py @@ -26,7 +26,7 @@ def add_chat_message(self, message: str) -> "Subsonic": :rtype: Self """ - self.api.request("addChatMessage", {"message": message}) + self.api.json_request("addChatMessage", {"message": message}) return self.subsonic @@ -37,7 +37,7 @@ def get_chat_messages(self) -> list[ChatMessage]: :rtype: list[ChatMessage] """ - response: list[dict[str, Any]] = self.api.request("getChatMessages")[ + response: list[dict[str, Any]] = self.api.json_request("getChatMessages")[ "chatMessages" ]["chatMessage"] diff --git a/src/knuckles/internet_radio.py b/src/knuckles/internet_radio.py index df4233a..6f85ff6 100644 --- a/src/knuckles/internet_radio.py +++ b/src/knuckles/internet_radio.py @@ -28,7 +28,7 @@ def get_internet_radio_stations( :rtype: list[InternetRadioStation] """ - response = self.api.request("getInternetRadioStations")[ + response = self.api.json_request("getInternetRadioStations")[ "internetRadioStations" ]["internetRadioStation"] @@ -67,7 +67,7 @@ def create_internet_radio_station( :rtype: Subsonic """ - self.api.request( + self.api.json_request( "createInternetRadioStation", {"streamUrl": stream_url, "name": name, "homepageUrl": homepage_url}, ) @@ -92,7 +92,7 @@ def update_internet_radio_station( :rtype: Subsonic """ - self.api.request( + self.api.json_request( "updateInternetRadioStation", { "id": id, @@ -113,6 +113,6 @@ def delete_internet_radio_station(self, id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("deleteInternetRadioStation", {"id": id}) + self.api.json_request("deleteInternetRadioStation", {"id": id}) return self.subsonic diff --git a/src/knuckles/jukebox.py b/src/knuckles/jukebox.py index 130dbb7..aed66f2 100644 --- a/src/knuckles/jukebox.py +++ b/src/knuckles/jukebox.py @@ -26,7 +26,7 @@ def get(self) -> Jukebox: :rtype: Jukebox """ - response = self.api.request("jukeboxControl", {"action": "get"})[ + response = self.api.json_request("jukeboxControl", {"action": "get"})[ "jukeboxPlaylist" ] @@ -40,7 +40,7 @@ def status(self) -> Jukebox: :rtype: Jukebox """ - response = self.api.request("jukeboxControl", {"action": "status"})[ + response = self.api.json_request("jukeboxControl", {"action": "status"})[ "jukeboxStatus" ] @@ -55,7 +55,7 @@ def set(self, id: str) -> Jukebox: :rtype: Jukebox """ - response = self.api.request("jukeboxControl", {"action": "set", "id": id})[ + response = self.api.json_request("jukeboxControl", {"action": "set", "id": id})[ "jukeboxStatus" ] @@ -70,7 +70,7 @@ def start(self) -> Jukebox: :rtype: Jukebox """ - response = self.api.request("jukeboxControl", {"action": "start"})[ + response = self.api.json_request("jukeboxControl", {"action": "start"})[ "jukeboxStatus" ] @@ -84,7 +84,7 @@ def stop(self) -> Jukebox: :rtype: Jukebox """ - response = self.api.request("jukeboxControl", {"action": "stop"})[ + response = self.api.json_request("jukeboxControl", {"action": "stop"})[ "jukeboxStatus" ] @@ -102,7 +102,7 @@ def skip(self, index: int, offset: float = 0) -> Jukebox: :rtype: Jukebox """ - response = self.api.request( + response = self.api.json_request( "jukeboxControl", {"action": "skip", "index": index, "offset": offset} )["jukeboxStatus"] @@ -118,7 +118,7 @@ def add(self, id: str) -> Jukebox: :rtype: Jukebox """ - response = self.api.request("jukeboxControl", {"action": "add", "id": id})[ + response = self.api.json_request("jukeboxControl", {"action": "add", "id": id})[ "jukeboxStatus" ] @@ -132,7 +132,7 @@ def clear(self) -> Jukebox: :rtype: Jukebox """ - response = self.api.request("jukeboxControl", {"action": "clear"})[ + response = self.api.json_request("jukeboxControl", {"action": "clear"})[ "jukeboxStatus" ] @@ -148,7 +148,7 @@ def remove(self, index: int) -> Jukebox: :rtype: Jukebox """ - response = self.api.request( + response = self.api.json_request( "jukeboxControl", {"action": "remove", "index": index} )["jukeboxStatus"] @@ -162,7 +162,7 @@ def shuffle(self) -> Jukebox: :rtype: Jukebox """ - response = self.api.request("jukeboxControl", {"action": "shuffle"})[ + response = self.api.json_request("jukeboxControl", {"action": "shuffle"})[ "jukeboxStatus" ] @@ -182,7 +182,7 @@ def set_gain(self, gain: float) -> Jukebox: if not 1 > gain > 0: raise ValueError("The gain should be between 0 and 1 (inclusive)") - response = self.api.request( + response = self.api.json_request( "jukeboxControl", {"action": "setGain", "gain": gain} )["jukeboxStatus"] diff --git a/src/knuckles/media_annotation.py b/src/knuckles/media_annotation.py index 95bec23..1f48fa6 100644 --- a/src/knuckles/media_annotation.py +++ b/src/knuckles/media_annotation.py @@ -27,33 +27,33 @@ def star_song(self, id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("star", {"id": id}) + self.api.json_request("star", {"id": id}) return self.subsonic def star_album(self, id: str) -> "Subsonic": """Calls the "star" endpoint of the API. - :param id: The ID of a album to star. + :param id: The ID of an album to star. :type id: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.request("star", {"albumId": id}) + self.api.json_request("star", {"albumId": id}) return self.subsonic def star_artist(self, id: str) -> "Subsonic": """Calls the "star" endpoint of the API. - :param id: The ID of a artist to star. + :param id: The ID of an artist to star. :type id: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.request("star", {"artistId": id}) + self.api.json_request("star", {"artistId": id}) return self.subsonic @@ -66,33 +66,33 @@ def unstar_song(self, id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("unstar", {"id": id}) + self.api.json_request("unstar", {"id": id}) return self.subsonic def unstar_album(self, id: str) -> "Subsonic": """Calls the "unstar" endpoint of the API. - :param id: The ID of a album to unstar. + :param id: The ID of an album to unstar. :type id: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.request("unstar", {"albumId": id}) + self.api.json_request("unstar", {"albumId": id}) return self.subsonic def unstar_artist(self, id: str) -> "Subsonic": """Calls the "unstar" endpoint of the API. - :param id: The ID of a artist to unstar. + :param id: The ID of an artist to unstar. :type id: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.request("unstar", {"artistId": id}) + self.api.json_request("unstar", {"artistId": id}) return self.subsonic @@ -118,7 +118,7 @@ def set_rating(self, id: str, rating: int) -> "Subsonic": ) ) - self.api.request("setRating", {"id": id, "rating": rating}) + self.api.json_request("setRating", {"id": id, "rating": rating}) return self.subsonic @@ -131,7 +131,7 @@ def remove_rating(self, id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("setRating", {"id": id, "rating": 0}) + self.api.json_request("setRating", {"id": id, "rating": 0}) return self.subsonic @@ -151,7 +151,7 @@ def scrobble( :rtype: Subsonic """ - self.api.request( + self.api.json_request( "scrobble", # Multiply by 1000 because the API uses # milliseconds instead of seconds for UNIX time diff --git a/src/knuckles/media_library_scanning.py b/src/knuckles/media_library_scanning.py index ddab81f..c3837c3 100644 --- a/src/knuckles/media_library_scanning.py +++ b/src/knuckles/media_library_scanning.py @@ -18,7 +18,7 @@ def get_scan_status(self) -> ScanStatus: :rtype: ScanStatus """ - response = self.api.request("getScanStatus")["scanStatus"] + response = self.api.json_request("getScanStatus")["scanStatus"] return ScanStatus(**response) @@ -29,6 +29,6 @@ def start_scan(self) -> ScanStatus: :rtype: ScanStatus """ - response = self.api.request("startScan")["scanStatus"] + response = self.api.json_request("startScan")["scanStatus"] return ScanStatus(**response) diff --git a/src/knuckles/media_retrieval.py b/src/knuckles/media_retrieval.py new file mode 100644 index 0000000..5c55a15 --- /dev/null +++ b/src/knuckles/media_retrieval.py @@ -0,0 +1,99 @@ +from pathlib import Path +from typing import Any + +from requests.models import PreparedRequest + +from .api import Api + + +class MediaRetrieval: + """Class that contains all the methods needed to interact + with the media retrieval calls in the Subsonic API. + + """ + + def __init__(self, api: Api) -> None: + self.api = api + + def _generate_url(self, endpoint: str, params: dict[str, Any]) -> str: + prepared_request = PreparedRequest() + prepared_request.prepare_url( + f"{self.api.url}/rest/{endpoint}", {**self.api.generate_params(), **params} + ) + + # Ignore the error caused by the url parameter of prepared_request + # as the prepare_url method always set it to a string. + return prepared_request.url # type: ignore [return-value] + + def stream(self, id: str) -> str: + """Returns a valid url for streaming the requested song + + :param id: The id of the song to stream + :type id: str + :return A url that points to the given song in the stream endpoint + :rtype str + """ + + return self._generate_url("stream", {"id": id}) + + def download(self, id: str, file_or_directory_path: Path) -> Path: + """Calls the "download" endpoint of the API. + + :param id: The id of the song or video to download. + :type id: str + :param file_or_directory_path: If a directory path is passed the file will be + inside of it with the default filename given by the API, + if not the file will be saved directly in the given path. + :type file_or_directory_path: Path + :return Returns the given path + :rtype Path + """ + + response = self.api.raw_request("download", {"id": id}) + response.raise_for_status() + + if file_or_directory_path.is_dir(): + filename = response.headers["Content-Disposition"].split("filename=")[1] + + # Remove leading quote char + if filename[0] == '"': + filename = filename[1:] + + # Remove trailing quote char + if filename[-1] == '"': + filename = filename[:-1] + + download_path = Path( + file_or_directory_path, + filename, + ) + else: + download_path = file_or_directory_path + + with open(download_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return download_path + + def hls(self, id: str) -> str: + """Returns a valid url for streaming the requested song with hls.m3u8 + + :param id: The id of the song to stream. + :type id: str + :return A url that points to the given song in the hls.m3u8 endpoint + :rtype str + """ + + return self._generate_url("hls.m3u8", {"id": id}) + + def get_captions(self) -> None: + ... + + def get_cover_art(self) -> None: + ... + + def get_lyrics(self) -> None: + ... + + def get_avatar(self) -> None: + ... diff --git a/src/knuckles/models/album.py b/src/knuckles/models/album.py index 40bb59f..9738e33 100644 --- a/src/knuckles/models/album.py +++ b/src/knuckles/models/album.py @@ -67,7 +67,6 @@ def generate(self) -> "AlbumInfo": return self.__subsonic.browsing.get_album_info(self.album_id) -# TODO Unfinished class Album: """Representation of all the data related to an album in Subsonic.""" diff --git a/src/knuckles/models/cover_art.py b/src/knuckles/models/cover_art.py index 7130ff5..61542b9 100644 --- a/src/knuckles/models/cover_art.py +++ b/src/knuckles/models/cover_art.py @@ -1,4 +1,3 @@ -# TODO Unfinished class CoverArt: """Representation of all the data related to a cover art in Subsonic.""" diff --git a/src/knuckles/models/play_queue.py b/src/knuckles/models/play_queue.py index 9b9601a..3a47e37 100644 --- a/src/knuckles/models/play_queue.py +++ b/src/knuckles/models/play_queue.py @@ -56,7 +56,6 @@ def save(self) -> Self: :rtype: Self """ - # TODO This should raise an exception? song_ids: list[str] = [song.id for song in self.songs] if self.songs else [] self.__subsonic.bookmarks.save_play_queue( diff --git a/src/knuckles/playlists.py b/src/knuckles/playlists.py index e829f57..0f9fc57 100644 --- a/src/knuckles/playlists.py +++ b/src/knuckles/playlists.py @@ -28,7 +28,7 @@ def get_playlists(self, username: str | None = None) -> list[Playlist]: :return: A list with all the playlist of the desired user. :rtype: list[Playlist] """ - response = self.api.request( + response = self.api.json_request( "getPlaylists", {"username": username} if username else {}, )["playlists"]["playlist"] @@ -45,7 +45,7 @@ def get_playlist(self, id: str) -> Playlist: :return: The requested playlist. :rtype: Playlist """ - response = self.api.request("getPlaylist", {"id": id})["playlist"] + response = self.api.json_request("getPlaylist", {"id": id})["playlist"] return Playlist(self.subsonic, **response) @@ -73,7 +73,7 @@ def create_playlist( :return: The new created playlist. :rtype: Playlist """ - response = self.api.request( + response = self.api.json_request( "createPlaylist", {"name": name, "songId": song_ids} )["playlist"] @@ -117,7 +117,7 @@ def update_playlist( :return: The updated version of the playlist. :rtype: Playlist """ - self.api.request( + self.api.json_request( "updatePlaylist", { "playlistId": id, @@ -139,6 +139,6 @@ def delete_playlist(self, id: str) -> "Subsonic": :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.request("deletePlaylist", {"id": id}) + self.api.json_request("deletePlaylist", {"id": id}) return self.subsonic diff --git a/src/knuckles/podcast.py b/src/knuckles/podcast.py index 039c7cd..9cc3e63 100644 --- a/src/knuckles/podcast.py +++ b/src/knuckles/podcast.py @@ -29,9 +29,9 @@ def get_podcasts(self, with_episodes: bool = True) -> list[Channel]: :rtype: list[Channel] """ - response = self.api.request("getPodcasts", {"includeEpisodes": with_episodes})[ - "podcasts" - ] + response = self.api.json_request( + "getPodcasts", {"includeEpisodes": with_episodes} + )["podcasts"] return [Channel(self.subsonic, **channel) for channel in response] @@ -48,7 +48,7 @@ def get_podcast(self, id: str, with_episodes: bool = True) -> Channel: :rtype: Channel """ - response = self.api.request( + response = self.api.json_request( "getPodcasts", {"id": id, "includeEpisodes": with_episodes} )["podcasts"][0] @@ -63,7 +63,7 @@ def get_newest_podcasts(self, number_max_episodes: int) -> list[Episode]: :rtype: list[Episode] """ - response = self.api.request( + response = self.api.json_request( "getNewestPodcasts", {"count": number_max_episodes} )["newestPodcasts"]["episode"] @@ -103,7 +103,7 @@ def refresh_podcasts(self) -> "Subsonic": :rtype: Subsonic """ - self.api.request("refreshPodcasts") + self.api.json_request("refreshPodcasts") return self.subsonic @@ -116,7 +116,7 @@ def create_podcast_channel(self, url: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("createPodcastChannel", {"url": url}) + self.api.json_request("createPodcastChannel", {"url": url}) return self.subsonic @@ -129,7 +129,7 @@ def delete_podcast_channel(self, id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("deletePodcastChannel", {"id": id}) + self.api.json_request("deletePodcastChannel", {"id": id}) return self.subsonic @@ -142,7 +142,7 @@ def download_podcast_episode(self, id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("downloadPodcastEpisode", {"id": id}) + self.api.json_request("downloadPodcastEpisode", {"id": id}) return self.subsonic @@ -155,6 +155,6 @@ def delete_podcast_episode(self, id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("deletePodcastEpisode", {"id": id}) + self.api.json_request("deletePodcastEpisode", {"id": id}) return self.subsonic diff --git a/src/knuckles/searching.py b/src/knuckles/searching.py index 00f162a..acf848d 100644 --- a/src/knuckles/searching.py +++ b/src/knuckles/searching.py @@ -63,7 +63,7 @@ def search( :return: :rtype: """ - response = self.api.request( + response = self.api.json_request( "search3", { "query": query, diff --git a/src/knuckles/sharing.py b/src/knuckles/sharing.py index a4ff130..93da17d 100644 --- a/src/knuckles/sharing.py +++ b/src/knuckles/sharing.py @@ -27,7 +27,7 @@ def get_shares(self) -> list[Share]: :rtype: list[Share] """ - response = self.api.request("getShares")["shares"]["share"] + response = self.api.json_request("getShares")["shares"]["share"] return [Share(self.subsonic, **share) for share in response] @@ -67,7 +67,7 @@ def create_share( :rtype: Share """ - response = self.api.request( + response = self.api.json_request( "createShare", { "id": songs_ids, @@ -96,7 +96,7 @@ def update_share( :rtype: Share """ - self.api.request( + self.api.json_request( "updateShare", { "id": share_id, @@ -122,6 +122,6 @@ def delete_share(self, share_id: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("deleteShare", {"id": share_id}) + self.api.json_request("deleteShare", {"id": share_id}) return self.subsonic diff --git a/src/knuckles/subsonic.py b/src/knuckles/subsonic.py index ad3ee10..7f9b66a 100644 --- a/src/knuckles/subsonic.py +++ b/src/knuckles/subsonic.py @@ -6,6 +6,7 @@ from .jukebox import JukeboxControl from .media_annotation import MediaAnnotation from .media_library_scanning import MediaLibraryScanning +from .media_retrieval import MediaRetrieval from .playlists import Playlists from .podcast import Podcast from .searching import Searching @@ -50,7 +51,7 @@ def __init__( self.lists = None #! ? self.searching = Searching(self.api, self) self.playlists = Playlists(self.api, self) - self.media_retrieval = None + self.media_retrieval = MediaRetrieval(self.api) self.media_annotation = MediaAnnotation(self.api, self) self.sharing = Sharing(self.api, self) self.podcast = Podcast(self.api, self) diff --git a/src/knuckles/system.py b/src/knuckles/system.py index 69c752f..5db3a37 100644 --- a/src/knuckles/system.py +++ b/src/knuckles/system.py @@ -28,7 +28,7 @@ def ping(self) -> SubsonicResponse: :rtype: SubsonicResponse """ - response = self.api.request("ping") + response = self.api.json_request("ping") return SubsonicResponse(**response) @@ -39,12 +39,12 @@ def get_license(self) -> License: :rtype: License """ - response = self.api.request("getLicense")["license"] + response = self.api.json_request("getLicense")["license"] return License(**response) def get_open_subsonic_extensions(self) -> list[OpenSubsonicExtension]: - response = self.api.request("getOpenSubsonicExtensions")[ + response = self.api.json_request("getOpenSubsonicExtensions")[ "openSubsonicExtensions" ] return [ diff --git a/src/knuckles/user_management.py b/src/knuckles/user_management.py index 64d534a..c154951 100644 --- a/src/knuckles/user_management.py +++ b/src/knuckles/user_management.py @@ -22,8 +22,6 @@ def __user_properties_to_json(user: User) -> dict[str, Any]: """Converts the data in a User object to a dictionary with the keys used in all the user related calls to the API. - # TODO Find a better approach to this. - :param user: The user to convert to a dictionary :type user: User :return: The dictionary with the data and valid keys to the API. @@ -56,7 +54,7 @@ def get_user(self, username: str) -> User: :rtype: User """ - request = self.api.request("getUser", {"username": username})["user"] + request = self.api.json_request("getUser", {"username": username})["user"] return User(subsonic=self.subsonic, **request) @@ -67,7 +65,7 @@ def get_users(self) -> list[User]: :rtype: list[User] """ - request = self.api.request("getUsers")["users"]["user"] + request = self.api.json_request("getUsers")["users"]["user"] users = [User(subsonic=self.subsonic, **user) for user in request] @@ -84,7 +82,7 @@ def create_user(self, new_user: User) -> User: user_json_data = self.__user_properties_to_json(new_user) - self.api.request("createUser", {**user_json_data}) + self.api.json_request("createUser", {**user_json_data}) # Attach the Subsonic object new_user.subsonic = self.subsonic @@ -105,7 +103,7 @@ def update_user(self, updated_data_user: User) -> User: user_json_data = self.__user_properties_to_json(updated_data_user) - self.api.request("updateUser", {**user_json_data}) + self.api.json_request("updateUser", {**user_json_data}) # Attach the Subsonic object updated_data_user.subsonic = self.subsonic @@ -121,7 +119,7 @@ def delete_user(self, username: str) -> "Subsonic": :rtype: Subsonic """ - self.api.request("deleteUser", {"username": username}) + self.api.json_request("deleteUser", {"username": username}) return self.subsonic @@ -136,7 +134,7 @@ def change_password(self, username: str, new_password: str) -> "Subsonic": :rtype: Self """ - self.api.request( + self.api.json_request( "changePassword", {"username": username, "password": new_password} ) diff --git a/tests/api/test_media_retrieval.py b/tests/api/test_media_retrieval.py new file mode 100644 index 0000000..2076dd7 --- /dev/null +++ b/tests/api/test_media_retrieval.py @@ -0,0 +1,65 @@ +from urllib import parse +from pathlib import Path +from typing import Any + + +import responses +from knuckles import Subsonic + +from tests.mocks.media_retrieval import MockDownload + + +def test_stream(subsonic: Subsonic, song: dict[str, Any]) -> None: + stream_url = parse.urlparse(subsonic.media_retrieval.stream(song["id"])) + + assert stream_url.path == "/rest/stream" + assert parse.parse_qs(stream_url.query)["id"][0] == song["id"] + + +@responses.activate +def test_download_with_a_given_filename( + tmp_path: Path, + output_filename: str, + placeholder_text: str, + mock_download: MockDownload, + subsonic: Subsonic, + song: dict[str, Any], +) -> None: + responses.add(mock_download(song["id"], tmp_path)) + + subsonic.media_retrieval.download(song["id"], tmp_path / output_filename) + + # Check if the file data has been unaltered + with open(tmp_path / output_filename, "r") as file: + assert placeholder_text == file.read() + + +@responses.activate +def test_download_without_a_given_filename( + tmp_path: Path, + input_filename: str, + placeholder_text: str, + mock_download: MockDownload, + subsonic: Subsonic, + song: dict[str, Any], +) -> None: + responses.add( + mock_download( + song["id"], + tmp_path, + headers={"Content-Disposition": f'attachment; filename="{input_filename}"'}, + ) + ) + + subsonic.media_retrieval.download(song["id"], tmp_path) + + # Check if the file data has been unaltered + with open(tmp_path / input_filename, "r") as file: + assert placeholder_text == file.read() + + +def test_hls(subsonic: Subsonic, song: dict[str, Any]) -> None: + stream_url = parse.urlparse(subsonic.media_retrieval.hls(song["id"])) + + assert stream_url.path == "/rest/hls.m3u8" + assert parse.parse_qs(stream_url.query)["id"][0] == song["id"] diff --git a/tests/conftest.py b/tests/conftest.py index a350c20..18689f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ "tests.mocks.internet_radio", "tests.mocks.bookmarks", "tests.mocks.searching", + "tests.mocks.media_retrieval", ] @@ -38,28 +39,41 @@ def __call__( endpoint: str, extra_params: dict[str, Any] = {}, extra_data: dict[str, Any] = {}, + headers: dict[str, str] = {}, + content_type: str = "", + body: Any = None, ) -> Response: ... @pytest.fixture def mock_generator( - params: dict[str, str], subsonic_response: dict[str, Any] + base_url: str, + params: dict[str, str], + subsonic_response: dict[str, Any], ) -> MockGenerator: def inner( endpoint: str, extra_params: dict[str, Any] = {}, extra_data: dict[str, Any] = {}, + headers: dict[str, str] = {}, + content_type: str = "", + body: Any = None, ) -> Response: return Response( method=GET, - url=f"https://example.com/rest/{endpoint}", + headers=headers, + content_type=content_type, + url=f"{base_url}/rest/{endpoint}", match=[ matchers.query_param_matcher( {**params, **extra_params}, strict_match=False ) ], - json={"subsonic-response": {**subsonic_response, **extra_data}}, + json={"subsonic-response": {**subsonic_response, **extra_data}} + if body is None + else None, + body=body, status=200, ) diff --git a/tests/mocks/browsing.py b/tests/mocks/browsing.py index 99a5dc5..a315944 100644 --- a/tests/mocks/browsing.py +++ b/tests/mocks/browsing.py @@ -31,7 +31,7 @@ def mock_get_genres(mock_generator: MockGenerator, genre: dict[str, Any]) -> Res @pytest.fixture() -def artist(album: dict[str, Any]) -> dict[str, Any]: +def artist(base_url: str, album: dict[str, Any]) -> dict[str, Any]: return { "id": "37ec820ca7193e17040c98f7da7c4b51", "name": "2 Mello", @@ -39,7 +39,7 @@ def artist(album: dict[str, Any]) -> dict[str, Any]: "albumCount": 1, "userRating": 5, "averageRating": 4.5, - "artistImageUrl": "https://example.com/artist.png", + "artistImageUrl": f"{base_url}/artist.png", "starred": "2017-04-11T10:42:50.842Z", "album": [album], } @@ -142,14 +142,14 @@ def mock_get_song(mock_generator: MockGenerator, song: dict[str, Any]) -> Respon @pytest.fixture -def album_info() -> dict[str, Any]: +def album_info(base_url: str) -> dict[str, Any]: return { "notes": "Example note", "musicBrainzId": "6e1d48f7-717c-416e-af35-5d2454a13af2", - "lastFmUrl": "https://example.com/lastfm/album", - "smallImageUrl": "https://example.com/album/small.png", - "mediumImageUrl": "https://example.com/album/medium.png", - "largeImageUrl": "https://example.com/album/large.png", + "lastFmUrl": f"{base_url}/lastfm/album", + "smallImageUrl": f"{base_url}/album/small.png", + "mediumImageUrl": f"{base_url}/album/medium.png", + "largeImageUrl": f"{base_url}/album/large.png", } diff --git a/tests/mocks/media_retrieval.py b/tests/mocks/media_retrieval.py new file mode 100644 index 0000000..1c6bb73 --- /dev/null +++ b/tests/mocks/media_retrieval.py @@ -0,0 +1,72 @@ +from pathlib import Path +from typing import Protocol + +import pytest +from responses import Response + +from tests.conftest import MockGenerator + + +@pytest.fixture() +def mock_stream(): + ... + + +class MockDownload(Protocol): + def __call__( + self, + song_id: str, + temp_dir_path: Path, + headers: dict[str, str] = {}, + ) -> Response: + ... + + +@pytest.fixture() +def placeholder_text() -> str: + return "Lorem Ipsum" + + +@pytest.fixture +def input_filename() -> str: + return "input.wav" + + +@pytest.fixture +def output_filename() -> str: + return "output.wav" + + +@pytest.fixture +def content_type() -> str: + return "audio/wav" + + +@pytest.fixture +def mock_download( + input_filename: str, + placeholder_text: str, + content_type: str, + mock_generator: MockGenerator, +) -> MockDownload: + def inner( + song_id: str, + temp_dir_path: Path, + headers: dict[str, str] = {}, + ): + fake_song = temp_dir_path / input_filename + fake_song.touch() + + with open(fake_song, "w") as file: + file.write(placeholder_text) + + with open(fake_song, "r") as file: + return mock_generator( + "download", + {"id": song_id}, + headers=headers, + content_type=content_type, + body=file.read(), + ) + + return inner diff --git a/tests/mocks/podcast.py b/tests/mocks/podcast.py index 7493f68..7a3f98a 100644 --- a/tests/mocks/podcast.py +++ b/tests/mocks/podcast.py @@ -31,14 +31,14 @@ def episode() -> dict[str, Any]: @pytest.fixture -def channel(episode: dict[str, Any]) -> dict[str, Any]: +def channel(base_url: str) -> dict[str, Any]: return { "id": "1", - "url": "https://example.com/podcasts/rss.xml", + "url": f"{base_url}/podcasts/rss.xml", "title": "Title", "description": "Description", "coverArt": "coverArtId", - "originalImageUrl": "https://example.com/podcasts/image.jpg", + "originalImageUrl": f"{base_url}/podcasts/image.jpg", "status": "completed", } diff --git a/tests/mocks/sharing.py b/tests/mocks/sharing.py index b05cdd3..6e3359d 100644 --- a/tests/mocks/sharing.py +++ b/tests/mocks/sharing.py @@ -8,10 +8,10 @@ @pytest.fixture -def share(song: dict[str, Any], username: str) -> dict[str, Any]: +def share(base_url: str, song: dict[str, Any], username: str) -> dict[str, Any]: return { "id": "12", - "url": "http://example.com/share/1", + "url": f"{base_url}/share/1", "description": "Forget and Remember (Comfort Fit)", "username": username, "created": "2020-04-16T04:12:09+00:00", diff --git a/tests/subsonic.py b/tests/subsonic.py index c485f4f..233ce2b 100644 --- a/tests/subsonic.py +++ b/tests/subsonic.py @@ -1,6 +1,5 @@ -import pytest - import knuckles +import pytest from knuckles.subsonic import Subsonic @@ -20,9 +19,14 @@ def client() -> str: @pytest.fixture -def subsonic(username: str, password: str, client: str) -> Subsonic: +def base_url() -> str: + return "https://example.com" + + +@pytest.fixture +def subsonic(base_url: str, username: str, password: str, client: str) -> Subsonic: return knuckles.Subsonic( - url="http://example.com", + url=base_url, user=username, password=password, client=client,