From 0b6cc5980fb74f25d778fe539419ab78dbfa48b7 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Thu, 26 Sep 2024 17:54:57 +0200 Subject: [PATCH 01/31] migrated project to use poetry --- .coveragerc | 30 ---- .gitignore | 3 +- README.md | 24 ++- coverage.sh | 3 - poetry.lock | 405 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 81 ++++++++++ requirements.txt | 7 - 7 files changed, 504 insertions(+), 49 deletions(-) delete mode 100644 .coveragerc delete mode 100755 coverage.sh create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 0974163..0000000 --- a/.coveragerc +++ /dev/null @@ -1,30 +0,0 @@ -[run] -source = youtube_transcript_api - - -[report] -omit = - */__main__.py - -exclude_lines = - pragma: no cover - - # Don't complain about missing debug-only code: - def __unicode__ - def __repr__ - if self\.debug - - # Don't complain if tests don't hit defensive assertion code: - raise AssertionError - raise NotImplementedError - - # Don't complain if non-runnable code isn't run: - if 0: - if __name__ == .__main__.: - - # Don't complain about empty stubs of abstract methods - @abstractmethod - @abstractclassmethod - @abstractstaticmethod - -show_missing = True \ No newline at end of file diff --git a/.gitignore b/.gitignore index 542d724..43b998b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ dist build *.egg-info upload_new_version.sh -.coverage \ No newline at end of file +.coverage +.DS_STORE \ No newline at end of file diff --git a/README.md b/README.md index d323d5e..980c565 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,6 @@ It is recommended to [install this module by using pip](https://pypi.org/project pip install youtube-transcript-api ``` -If you want to use it from source, you'll have to install the dependencies manually: - -``` -pip install -r requirements.txt -``` - You can either integrate this module [into an existing application](#api) or just use it via a [CLI](#cli). ## API @@ -371,10 +365,24 @@ Using the CLI: youtube_transcript_api --cookies /path/to/your/cookies.txt ``` - ## Warning - This code uses an undocumented part of the YouTube API, which is called by the YouTube web-client. So there is no guarantee that it won't stop working tomorrow, if they change how things work. I will however do my best to make things working again as soon as possible if that happens. So if it stops working, let me know! +This code uses an undocumented part of the YouTube API, which is called by the YouTube web-client. So there is no guarantee that it won't stop working tomorrow, if they change how things work. I will however do my best to make things working again as soon as possible if that happens. So if it stops working, let me know! + +## Contributing + +To setup the project locally run (requires [poetry](https://python-poetry.org/docs/) to be installed): +```shell +poetry install +``` + +There's [poe](https://github.com/nat-n/poethepoet?tab=readme-ov-file#quick-start) tasks to run tests, coverage, the linter and formatter (you'll need to pass all of those for the build to pass): +```shell +poe test +poe coverage +poe format +poe lint +``` ## Donations diff --git a/coverage.sh b/coverage.sh deleted file mode 100755 index c7fe42b..0000000 --- a/coverage.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -.venv/bin/coverage run -m unittest discover && .venv/bin/coverage report diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f87bcde --- /dev/null +++ b/poetry.lock @@ -0,0 +1,405 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +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.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[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 = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "coveralls" +version = "4.0.1" +description = "Show coverage stats online via coveralls.io" +optional = false +python-versions = "<3.13,>=3.8" +files = [ + {file = "coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809"}, + {file = "coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69"}, +] + +[package.dependencies] +coverage = {version = ">=5.0,<6.0.dev0 || >6.1,<6.1.1 || >6.1.1,<8.0", extras = ["toml"]} +docopt = ">=0.6.1,<0.7.0" +requests = ">=1.0.0,<3.0.0" + +[package.extras] +yaml = ["pyyaml (>=3.10,<7.0)"] + +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +optional = false +python-versions = "*" +files = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "httpretty" +version = "1.1.4" +description = "HTTP client mock for Python" +optional = false +python-versions = ">=3" +files = [ + {file = "httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[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 = "mock" +version = "5.1.0" +description = "Rolling backport of unittest.mock for all Pythons" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"}, + {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"}, +] + +[package.extras] +build = ["blurb", "twine", "wheel"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[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 = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8,<3.13" +content-hash = "ae3ea36431a2a24e1d07e7c6e251fe7490b86edd928c22eda084e3cb974aaa99" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5176f40 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "youtube-transcript-api" +version = "0.6.2" +description = "This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. It also works for automatically generated subtitles, supports translating subtitles and it does not require a headless browser, like other selenium based solutions do!" +readme = "README.md" +license = "MIT" +authors = [ + "Jonas Depoix ", +] +homepage = "https://github.com/jdepoix/youtube-transcript-api" +repository = "https://github.com/jdepoix/youtube-transcript-api" +keywords = [ + "cli", + "subtitle", + "subtitles", + "transcript", + "transcripts", + "youtube", + "youtube-api", + "youtube-subtitles", + "youtube-transcripts", +] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[tool.poetry.scripts] +youtube_transcript_api = "youtube_transcript_api.__main__:main" + +[tool.poe.tasks] +test = "pytest youtube_transcript_api" +coverage.shell = "pytest youtube_transcript_api && coverage report -m" + +[tool.poetry.dependencies] +python = ">=3.8,<3.13" +requests = "*" + +[tool.poetry.group.test.dependencies] +pytest = "^8.3.3" +coverage = "^7.6.1" +mock = "^5.1.0" +httpretty = "^1.1.4" +coveralls = "^4.0.1" + +[tool.coverage.run] +source = ["youtube_transcript_api"] + +[tool.coverage.report] +omit = ["*/__main__.py", "youtube_transcript_api/test/*"] +exclude_lines = [ + "pragma: no cover", + + # Don't complain about missing debug-only code: + "def __unicode__", + "def __repr__", + "if self\\.debug", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + + # Don't complain about empty stubs of abstract methods + "@abstractmethod", + "@abstractclassmethod", + "@abstractstaticmethod" +] +show_missing = true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 62104e5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -requests - -# testing -mock==3.0.5 -httpretty==1.1.4 -coveralls==1.11.1 -coverage==5.2.1 From 5f96588ada0d30ac90ff3d3a0031a5bfe022a177 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Thu, 26 Sep 2024 17:56:36 +0200 Subject: [PATCH 02/31] added black formatter --- poetry.lock | 111 +++++- pyproject.toml | 2 + youtube_transcript_api/__main__.py | 2 +- youtube_transcript_api/_api.py | 42 ++- youtube_transcript_api/_cli.py | 114 +++--- youtube_transcript_api/_errors.py | 63 ++-- youtube_transcript_api/_html_unescaping.py | 4 +- youtube_transcript_api/_settings.py | 2 +- youtube_transcript_api/_transcripts.py | 157 ++++---- youtube_transcript_api/formatters.py | 78 ++-- youtube_transcript_api/test/test_api.py | 313 +++++++++------- youtube_transcript_api/test/test_cli.py | 336 +++++++++++------- .../test/test_formatters.py | 50 +-- 13 files changed, 804 insertions(+), 470 deletions(-) diff --git a/poetry.lock b/poetry.lock index f87bcde..b3fc681 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,51 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "black" +version = "24.8.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2024.8.30" @@ -110,6 +156,20 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -302,6 +362,17 @@ build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest", "pytest-cov"] +[[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 = "24.1" @@ -313,6 +384,33 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -382,6 +480,17 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + [[package]] name = "urllib3" version = "2.2.3" @@ -402,4 +511,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "ae3ea36431a2a24e1d07e7c6e251fe7490b86edd928c22eda084e3cb974aaa99" +content-hash = "4c2e7d294773ea148b69f961053a9469630c48b88248903ead43e41a2838ff94" diff --git a/pyproject.toml b/pyproject.toml index 5176f40..5720af7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" [tool.poe.tasks] test = "pytest youtube_transcript_api" coverage.shell = "pytest youtube_transcript_api && coverage report -m" +format = "black youtube_transcript_api" [tool.poetry.dependencies] python = ">=3.8,<3.13" @@ -51,6 +52,7 @@ coverage = "^7.6.1" mock = "^5.1.0" httpretty = "^1.1.4" coveralls = "^4.0.1" +black = "^24.8.0" [tool.coverage.run] source = ["youtube_transcript_api"] diff --git a/youtube_transcript_api/__main__.py b/youtube_transcript_api/__main__.py index f756560..5b96393 100644 --- a/youtube_transcript_api/__main__.py +++ b/youtube_transcript_api/__main__.py @@ -11,5 +11,5 @@ def main(): print(YouTubeTranscriptCli(sys.argv[1:]).run()) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/youtube_transcript_api/_api.py b/youtube_transcript_api/_api.py index 24a1236..bf1f240 100644 --- a/youtube_transcript_api/_api.py +++ b/youtube_transcript_api/_api.py @@ -1,17 +1,17 @@ import requests -try: # pragma: no cover + +try: # pragma: no cover import http.cookiejar as cookiejar + CookieLoadError = (FileNotFoundError, cookiejar.LoadError) -except ImportError: # pragma: no cover +except ImportError: # pragma: no cover import cookielib as cookiejar + CookieLoadError = IOError from ._transcripts import TranscriptListFetcher -from ._errors import ( - CookiePathInvalid, - CookiesInvalid -) +from ._errors import CookiePathInvalid, CookiesInvalid class YouTubeTranscriptApi(object): @@ -71,8 +71,15 @@ def list_transcripts(cls, video_id, proxies=None, cookies=None): return TranscriptListFetcher(http_client).fetch(video_id) @classmethod - def get_transcripts(cls, video_ids, languages=('en',), continue_after_error=False, proxies=None, - cookies=None, preserve_formatting=False): + def get_transcripts( + cls, + video_ids, + languages=("en",), + continue_after_error=False, + proxies=None, + cookies=None, + preserve_formatting=False, + ): """ Retrieves the transcripts for a list of videos. @@ -102,7 +109,9 @@ def get_transcripts(cls, video_ids, languages=('en',), continue_after_error=Fals for video_id in video_ids: try: - data[video_id] = cls.get_transcript(video_id, languages, proxies, cookies, preserve_formatting) + data[video_id] = cls.get_transcript( + video_id, languages, proxies, cookies, preserve_formatting + ) except Exception as exception: if not continue_after_error: raise exception @@ -112,7 +121,14 @@ def get_transcripts(cls, video_ids, languages=('en',), continue_after_error=Fals return data, unretrievable_videos @classmethod - def get_transcript(cls, video_id, languages=('en',), proxies=None, cookies=None, preserve_formatting=False): + def get_transcript( + cls, + video_id, + languages=("en",), + proxies=None, + cookies=None, + preserve_formatting=False, + ): """ Retrieves the transcript for a single video. This is just a shortcut for calling:: @@ -134,7 +150,11 @@ def get_transcript(cls, video_id, languages=('en',), proxies=None, cookies=None, :rtype [{'text': str, 'start': float, 'end': float}]: """ assert isinstance(video_id, str), "`video_id` must be a string" - return cls.list_transcripts(video_id, proxies, cookies).find_transcript(languages).fetch(preserve_formatting=preserve_formatting) + return ( + cls.list_transcripts(video_id, proxies, cookies) + .find_transcript(languages) + .fetch(preserve_formatting=preserve_formatting) + ) @classmethod def _load_cookies(cls, cookies, video_id): diff --git a/youtube_transcript_api/_cli.py b/youtube_transcript_api/_cli.py index a9cbf75..09f76ba 100644 --- a/youtube_transcript_api/_cli.py +++ b/youtube_transcript_api/_cli.py @@ -13,10 +13,10 @@ def run(self): parsed_args = self._parse_args() if parsed_args.exclude_manually_created and parsed_args.exclude_generated: - return '' + return "" proxies = None - if parsed_args.http_proxy != '' or parsed_args.https_proxy != '': + if parsed_args.http_proxy != "" or parsed_args.https_proxy != "": proxies = {"http": parsed_args.http_proxy, "https": parsed_args.https_proxy} cookies = parsed_args.cookies @@ -26,25 +26,41 @@ def run(self): for video_id in parsed_args.video_ids: try: - transcripts.append(self._fetch_transcript(parsed_args, proxies, cookies, video_id)) + transcripts.append( + self._fetch_transcript(parsed_args, proxies, cookies, video_id) + ) except Exception as exception: exceptions.append(exception) - return '\n\n'.join( + return "\n\n".join( [str(exception) for exception in exceptions] - + ([FormatterLoader().load(parsed_args.format).format_transcripts(transcripts)] if transcripts else []) + + ( + [ + FormatterLoader() + .load(parsed_args.format) + .format_transcripts(transcripts) + ] + if transcripts + else [] + ) ) def _fetch_transcript(self, parsed_args, proxies, cookies, video_id): - transcript_list = YouTubeTranscriptApi.list_transcripts(video_id, proxies=proxies, cookies=cookies) + transcript_list = YouTubeTranscriptApi.list_transcripts( + video_id, proxies=proxies, cookies=cookies + ) if parsed_args.list_transcripts: return str(transcript_list) if parsed_args.exclude_manually_created: - transcript = transcript_list.find_generated_transcript(parsed_args.languages) + transcript = transcript_list.find_generated_transcript( + parsed_args.languages + ) elif parsed_args.exclude_generated: - transcript = transcript_list.find_manually_created_transcript(parsed_args.languages) + transcript = transcript_list.find_manually_created_transcript( + parsed_args.languages + ) else: transcript = transcript_list.find_transcript(parsed_args.languages) @@ -56,80 +72,84 @@ def _fetch_transcript(self, parsed_args, proxies, cookies, video_id): def _parse_args(self): parser = argparse.ArgumentParser( description=( - 'This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. ' - 'It also works for automatically generated subtitles and it does not require a headless browser, like ' - 'other selenium based solutions do!' + "This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. " + "It also works for automatically generated subtitles and it does not require a headless browser, like " + "other selenium based solutions do!" ) ) parser.add_argument( - '--list-transcripts', - action='store_const', + "--list-transcripts", + action="store_const", const=True, default=False, - help='This will list the languages in which the given videos are available in.', + help="This will list the languages in which the given videos are available in.", ) - parser.add_argument('video_ids', nargs='+', type=str, help='List of YouTube video IDs.') parser.add_argument( - '--languages', - nargs='*', - default=['en',], + "video_ids", nargs="+", type=str, help="List of YouTube video IDs." + ) + parser.add_argument( + "--languages", + nargs="*", + default=[ + "en", + ], type=str, help=( 'A list of language codes in a descending priority. For example, if this is set to "de en" it will ' - 'first try to fetch the german transcript (de) and then fetch the english transcript (en) if it fails ' - 'to do so. As I can\'t provide a complete list of all working language codes with full certainty, you ' - 'may have to play around with the language codes a bit, to find the one which is working for you!' + "first try to fetch the german transcript (de) and then fetch the english transcript (en) if it fails " + "to do so. As I can't provide a complete list of all working language codes with full certainty, you " + "may have to play around with the language codes a bit, to find the one which is working for you!" ), ) parser.add_argument( - '--exclude-generated', - action='store_const', + "--exclude-generated", + action="store_const", const=True, default=False, - help='If this flag is set transcripts which have been generated by YouTube will not be retrieved.', + help="If this flag is set transcripts which have been generated by YouTube will not be retrieved.", ) parser.add_argument( - '--exclude-manually-created', - action='store_const', + "--exclude-manually-created", + action="store_const", const=True, default=False, - help='If this flag is set transcripts which have been manually created will not be retrieved.', + help="If this flag is set transcripts which have been manually created will not be retrieved.", ) parser.add_argument( - '--format', + "--format", type=str, - default='pretty', + default="pretty", choices=tuple(FormatterLoader.TYPES.keys()), ) parser.add_argument( - '--translate', - default='', + "--translate", + default="", help=( - 'The language code for the language you want this transcript to be translated to. Use the ' - '--list-transcripts feature to find out which languages are translatable and which translation ' - 'languages are available.' - ) + "The language code for the language you want this transcript to be translated to. Use the " + "--list-transcripts feature to find out which languages are translatable and which translation " + "languages are available." + ), ) parser.add_argument( - '--http-proxy', - default='', - metavar='URL', - help='Use the specified HTTP proxy.' + "--http-proxy", + default="", + metavar="URL", + help="Use the specified HTTP proxy.", ) parser.add_argument( - '--https-proxy', - default='', - metavar='URL', - help='Use the specified HTTPS proxy.' + "--https-proxy", + default="", + metavar="URL", + help="Use the specified HTTPS proxy.", ) parser.add_argument( - '--cookies', + "--cookies", default=None, - help='The cookie file that will be used for authorization with youtube.' + help="The cookie file that will be used for authorization with youtube.", ) - + return self._sanitize_video_ids(parser.parse_args(self._args)) def _sanitize_video_ids(self, args): - args.video_ids = [video_id.replace('\\', '') for video_id in args.video_ids] + args.video_ids = [video_id.replace("\\", "") for video_id in args.video_ids] return args diff --git a/youtube_transcript_api/_errors.py b/youtube_transcript_api/_errors.py index d652c59..df4b0ad 100644 --- a/youtube_transcript_api/_errors.py +++ b/youtube_transcript_api/_errors.py @@ -5,16 +5,17 @@ class CouldNotRetrieveTranscript(Exception): """ Raised if a transcript could not be retrieved. """ - ERROR_MESSAGE = '\nCould not retrieve a transcript for the video {video_url}!' - CAUSE_MESSAGE_INTRO = ' This is most likely caused by:\n\n{cause}' - CAUSE_MESSAGE = '' + + ERROR_MESSAGE = "\nCould not retrieve a transcript for the video {video_url}!" + CAUSE_MESSAGE_INTRO = " This is most likely caused by:\n\n{cause}" + CAUSE_MESSAGE = "" GITHUB_REFERRAL = ( - '\n\nIf you are sure that the described cause is not responsible for this error ' - 'and that a transcript should be retrievable, please create an issue at ' - 'https://github.com/jdepoix/youtube-transcript-api/issues. ' - 'Please add which version of youtube_transcript_api you are using ' - 'and provide the information needed to replicate the error. ' - 'Also make sure that there are no open issues which already describe your problem!' + "\n\nIf you are sure that the described cause is not responsible for this error " + "and that a transcript should be retrievable, please create an issue at " + "https://github.com/jdepoix/youtube-transcript-api/issues. " + "Please add which version of youtube_transcript_api you are using " + "and provide the information needed to replicate the error. " + "Also make sure that there are no open issues which already describe your problem!" ) def __init__(self, video_id): @@ -23,10 +24,14 @@ def __init__(self, video_id): def _build_error_message(self): cause = self.cause - error_message = self.ERROR_MESSAGE.format(video_url=WATCH_URL.format(video_id=self.video_id)) + error_message = self.ERROR_MESSAGE.format( + video_url=WATCH_URL.format(video_id=self.video_id) + ) if cause: - error_message += self.CAUSE_MESSAGE_INTRO.format(cause=cause) + self.GITHUB_REFERRAL + error_message += ( + self.CAUSE_MESSAGE_INTRO.format(cause=cause) + self.GITHUB_REFERRAL + ) return error_message @@ -36,7 +41,7 @@ def cause(self): class YouTubeRequestFailed(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'Request to YouTube failed: {reason}' + CAUSE_MESSAGE = "Request to YouTube failed: {reason}" def __init__(self, video_id, http_error): self.reason = str(http_error) @@ -50,12 +55,12 @@ def cause(self): class VideoUnavailable(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'The video is no longer available' + CAUSE_MESSAGE = "The video is no longer available" class InvalidVideoId(CouldNotRetrieveTranscript): CAUSE_MESSAGE = ( - 'You provided an invalid video id. Make sure you are using the video id and NOT the url!\n\n' + "You provided an invalid video id. Make sure you are using the video id and NOT the url!\n\n" 'Do NOT run: `YouTubeTranscriptApi.get_transcript("https://www.youtube.com/watch?v=1234")`\n' 'Instead run: `YouTubeTranscriptApi.get_transcript("1234")`' ) @@ -63,48 +68,48 @@ class InvalidVideoId(CouldNotRetrieveTranscript): class TooManyRequests(CouldNotRetrieveTranscript): CAUSE_MESSAGE = ( - 'YouTube is receiving too many requests from this IP and now requires solving a captcha to continue. ' - 'One of the following things can be done to work around this:\n\ - - Manually solve the captcha in a browser and export the cookie. ' - 'Read here how to use that cookie with ' - 'youtube-transcript-api: https://github.com/jdepoix/youtube-transcript-api#cookies\n\ + "YouTube is receiving too many requests from this IP and now requires solving a captcha to continue. " + "One of the following things can be done to work around this:\n\ + - Manually solve the captcha in a browser and export the cookie. " + "Read here how to use that cookie with " + "youtube-transcript-api: https://github.com/jdepoix/youtube-transcript-api#cookies\n\ - Use a different IP address\n\ - - Wait until the ban on your IP has been lifted' + - Wait until the ban on your IP has been lifted" ) class TranscriptsDisabled(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'Subtitles are disabled for this video' + CAUSE_MESSAGE = "Subtitles are disabled for this video" class NoTranscriptAvailable(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'No transcripts are available for this video' + CAUSE_MESSAGE = "No transcripts are available for this video" class NotTranslatable(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'The requested language is not translatable' + CAUSE_MESSAGE = "The requested language is not translatable" class TranslationLanguageNotAvailable(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'The requested translation language is not available' + CAUSE_MESSAGE = "The requested translation language is not available" class CookiePathInvalid(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'The provided cookie file was unable to be loaded' + CAUSE_MESSAGE = "The provided cookie file was unable to be loaded" class CookiesInvalid(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'The cookies provided are not valid (may have expired)' + CAUSE_MESSAGE = "The cookies provided are not valid (may have expired)" class FailedToCreateConsentCookie(CouldNotRetrieveTranscript): - CAUSE_MESSAGE = 'Failed to automatically give consent to saving cookies' + CAUSE_MESSAGE = "Failed to automatically give consent to saving cookies" class NoTranscriptFound(CouldNotRetrieveTranscript): CAUSE_MESSAGE = ( - 'No transcripts were found for any of the requested language codes: {requested_language_codes}\n\n' - '{transcript_data}' + "No transcripts were found for any of the requested language codes: {requested_language_codes}\n\n" + "{transcript_data}" ) def __init__(self, video_id, requested_language_codes, transcript_data): diff --git a/youtube_transcript_api/_html_unescaping.py b/youtube_transcript_api/_html_unescaping.py index 3efdf4b..6654d70 100644 --- a/youtube_transcript_api/_html_unescaping.py +++ b/youtube_transcript_api/_html_unescaping.py @@ -2,10 +2,10 @@ # This can only be tested by using different python versions, therefore it is not covered by coverage.py -if sys.version_info.major == 3 and sys.version_info.minor >= 4: # pragma: no cover +if sys.version_info.major == 3 and sys.version_info.minor >= 4: # pragma: no cover # Python 3.4+ from html import unescape -else: # pragma: no cover +else: # pragma: no cover if sys.version_info.major <= 2: # Python 2 import HTMLParser diff --git a/youtube_transcript_api/_settings.py b/youtube_transcript_api/_settings.py index b1f7dfe..585b863 100644 --- a/youtube_transcript_api/_settings.py +++ b/youtube_transcript_api/_settings.py @@ -1 +1 @@ -WATCH_URL = 'https://www.youtube.com/watch?v={video_id}' +WATCH_URL = "https://www.youtube.com/watch?v={video_id}" diff --git a/youtube_transcript_api/_transcripts.py b/youtube_transcript_api/_transcripts.py index ef1f44b..7ce4d2e 100644 --- a/youtube_transcript_api/_transcripts.py +++ b/youtube_transcript_api/_transcripts.py @@ -3,7 +3,7 @@ # This can only be tested by using different python versions, therefore it is not covered by coverage.py if sys.version_info.major == 2: # pragma: no cover reload(sys) - sys.setdefaultencoding('utf-8') + sys.setdefaultencoding("utf-8") import json @@ -52,7 +52,7 @@ def _extract_captions_json(self, html, video_id): splitted_html = html.split('"captions":') if len(splitted_html) <= 1: - if video_id.startswith('http://') or video_id.startswith('https://'): + if video_id.startswith("http://") or video_id.startswith("https://"): raise InvalidVideoId(video_id) if 'class="g-recaptcha"' in html: raise TooManyRequests(video_id) @@ -62,12 +62,12 @@ def _extract_captions_json(self, html, video_id): raise TranscriptsDisabled(video_id) captions_json = json.loads( - splitted_html[1].split(',"videoDetails')[0].replace('\n', '') - ).get('playerCaptionsTracklistRenderer') + splitted_html[1].split(',"videoDetails')[0].replace("\n", "") + ).get("playerCaptionsTracklistRenderer") if captions_json is None: raise TranscriptsDisabled(video_id) - if 'captionTracks' not in captions_json: + if "captionTracks" not in captions_json: raise NoTranscriptAvailable(video_id) return captions_json @@ -76,7 +76,9 @@ def _create_consent_cookie(self, html, video_id): match = re.search('name="v" value="(.*?)"', html) if match is None: raise FailedToCreateConsentCookie(video_id) - self._http_client.cookies.set('CONSENT', 'YES+' + match.group(1), domain='.youtube.com') + self._http_client.cookies.set( + "CONSENT", "YES+" + match.group(1), domain=".youtube.com" + ) def _fetch_video_html(self, video_id): html = self._fetch_html(video_id) @@ -88,7 +90,9 @@ def _fetch_video_html(self, video_id): return html def _fetch_html(self, video_id): - response = self._http_client.get(WATCH_URL.format(video_id=video_id), headers={'Accept-Language': 'en-US'}) + response = self._http_client.get( + WATCH_URL.format(video_id=video_id), headers={"Accept-Language": "en-US"} + ) return unescape(_raise_http_errors(response, video_id).text) @@ -98,7 +102,13 @@ class TranscriptList(object): for a given YouTube video. Also it provides functionality to search for a transcript in a given language. """ - def __init__(self, video_id, manually_created_transcripts, generated_transcripts, translation_languages): + def __init__( + self, + video_id, + manually_created_transcripts, + generated_transcripts, + translation_languages, + ): """ The constructor is only for internal use. Use the static build method instead. @@ -132,28 +142,29 @@ def build(http_client, video_id, captions_json): """ translation_languages = [ { - 'language': translation_language['languageName']['simpleText'], - 'language_code': translation_language['languageCode'], - } for translation_language in captions_json.get('translationLanguages', []) + "language": translation_language["languageName"]["simpleText"], + "language_code": translation_language["languageCode"], + } + for translation_language in captions_json.get("translationLanguages", []) ] manually_created_transcripts = {} generated_transcripts = {} - for caption in captions_json['captionTracks']: - if caption.get('kind', '') == 'asr': + for caption in captions_json["captionTracks"]: + if caption.get("kind", "") == "asr": transcript_dict = generated_transcripts else: transcript_dict = manually_created_transcripts - transcript_dict[caption['languageCode']] = Transcript( + transcript_dict[caption["languageCode"]] = Transcript( http_client, video_id, - caption['baseUrl'], - caption['name']['simpleText'], - caption['languageCode'], - caption.get('kind', '') == 'asr', - translation_languages if caption.get('isTranslatable', False) else [], + caption["baseUrl"], + caption["name"]["simpleText"], + caption["languageCode"], + caption.get("kind", "") == "asr", + translation_languages if caption.get("isTranslatable", False) else [], ) return TranscriptList( @@ -164,7 +175,10 @@ def build(http_client, video_id, captions_json): ) def __iter__(self): - return iter(list(self._manually_created_transcripts.values()) + list(self._generated_transcripts.values())) + return iter( + list(self._manually_created_transcripts.values()) + + list(self._generated_transcripts.values()) + ) def find_transcript(self, language_codes): """ @@ -180,7 +194,10 @@ def find_transcript(self, language_codes): :rtype Transcript: :raises: NoTranscriptFound """ - return self._find_transcript(language_codes, [self._manually_created_transcripts, self._generated_transcripts]) + return self._find_transcript( + language_codes, + [self._manually_created_transcripts, self._generated_transcripts], + ) def find_generated_transcript(self, language_codes): """ @@ -208,7 +225,9 @@ def find_manually_created_transcript(self, language_codes): :rtype Transcript: :raises: NoTranscriptFound """ - return self._find_transcript(language_codes, [self._manually_created_transcripts]) + return self._find_transcript( + language_codes, [self._manually_created_transcripts] + ) def _find_transcript(self, language_codes, transcript_dicts): for language_code in language_codes: @@ -216,44 +235,54 @@ def _find_transcript(self, language_codes, transcript_dicts): if language_code in transcript_dict: return transcript_dict[language_code] - raise NoTranscriptFound( - self.video_id, - language_codes, - self - ) + raise NoTranscriptFound(self.video_id, language_codes, self) def __str__(self): return ( - 'For this video ({video_id}) transcripts are available in the following languages:\n\n' - '(MANUALLY CREATED)\n' - '{available_manually_created_transcript_languages}\n\n' - '(GENERATED)\n' - '{available_generated_transcripts}\n\n' - '(TRANSLATION LANGUAGES)\n' - '{available_translation_languages}' + "For this video ({video_id}) transcripts are available in the following languages:\n\n" + "(MANUALLY CREATED)\n" + "{available_manually_created_transcript_languages}\n\n" + "(GENERATED)\n" + "{available_generated_transcripts}\n\n" + "(TRANSLATION LANGUAGES)\n" + "{available_translation_languages}" ).format( video_id=self.video_id, available_manually_created_transcript_languages=self._get_language_description( - str(transcript) for transcript in self._manually_created_transcripts.values() + str(transcript) + for transcript in self._manually_created_transcripts.values() ), available_generated_transcripts=self._get_language_description( str(transcript) for transcript in self._generated_transcripts.values() ), available_translation_languages=self._get_language_description( '{language_code} ("{language}")'.format( - language=translation_language['language'], - language_code=translation_language['language_code'], - ) for translation_language in self._translation_languages - ) + language=translation_language["language"], + language_code=translation_language["language_code"], + ) + for translation_language in self._translation_languages + ), ) def _get_language_description(self, transcript_strings): - description = '\n'.join(' - {transcript}'.format(transcript=transcript) for transcript in transcript_strings) - return description if description else 'None' + description = "\n".join( + " - {transcript}".format(transcript=transcript) + for transcript in transcript_strings + ) + return description if description else "None" class Transcript(object): - def __init__(self, http_client, video_id, url, language, language_code, is_generated, translation_languages): + def __init__( + self, + http_client, + video_id, + url, + language, + language_code, + is_generated, + translation_languages, + ): """ You probably don't want to initialize this directly. Usually you'll access Transcript objects using a TranscriptList. @@ -276,7 +305,7 @@ def __init__(self, http_client, video_id, url, language, language_code, is_gener self.is_generated = is_generated self.translation_languages = translation_languages self._translation_languages_dict = { - translation_language['language_code']: translation_language['language'] + translation_language["language_code"]: translation_language["language"] for translation_language in translation_languages } @@ -288,7 +317,9 @@ def fetch(self, preserve_formatting=False): :return: a list of dictionaries containing the 'text', 'start' and 'duration' keys :rtype [{'text': str, 'start': float, 'end': float}]: """ - response = self._http_client.get(self._url, headers={'Accept-Language': 'en-US'}) + response = self._http_client.get( + self._url, headers={"Accept-Language": "en-US"} + ) return _TranscriptParser(preserve_formatting=preserve_formatting).parse( _raise_http_errors(response, self.video_id).text, ) @@ -297,7 +328,7 @@ def __str__(self): return '{language_code} ("{language}"){translation_description}'.format( language=self.language, language_code=self.language_code, - translation_description='[TRANSLATABLE]' if self.is_translatable else '' + translation_description="[TRANSLATABLE]" if self.is_translatable else "", ) @property @@ -314,7 +345,9 @@ def translate(self, language_code): return Transcript( self._http_client, self.video_id, - '{url}&tlang={language_code}'.format(url=self._url, language_code=language_code), + "{url}&tlang={language_code}".format( + url=self._url, language_code=language_code + ), self._translation_languages_dict[language_code], language_code, True, @@ -324,16 +357,16 @@ def translate(self, language_code): class _TranscriptParser(object): _FORMATTING_TAGS = [ - 'strong', # important - 'em', # emphasized - 'b', # bold - 'i', # italic - 'mark', # marked - 'small', # smaller - 'del', # deleted - 'ins', # inserted - 'sub', # subscript - 'sup', # superscript + "strong", # important + "em", # emphasized + "b", # bold + "i", # italic + "mark", # marked + "small", # smaller + "del", # deleted + "ins", # inserted + "sub", # subscript + "sup", # superscript ] def __init__(self, preserve_formatting=False): @@ -341,19 +374,19 @@ def __init__(self, preserve_formatting=False): def _get_html_regex(self, preserve_formatting): if preserve_formatting: - formats_regex = '|'.join(self._FORMATTING_TAGS) - formats_regex = r'<\/?(?!\/?(' + formats_regex + r')\b).*?\b>' + formats_regex = "|".join(self._FORMATTING_TAGS) + formats_regex = r"<\/?(?!\/?(" + formats_regex + r")\b).*?\b>" html_regex = re.compile(formats_regex, re.IGNORECASE) else: - html_regex = re.compile(r'<[^>]*>', re.IGNORECASE) + html_regex = re.compile(r"<[^>]*>", re.IGNORECASE) return html_regex def parse(self, plain_data): return [ { - 'text': re.sub(self._html_regex, '', unescape(xml_element.text)), - 'start': float(xml_element.attrib['start']), - 'duration': float(xml_element.attrib.get('dur', '0.0')), + "text": re.sub(self._html_regex, "", unescape(xml_element.text)), + "start": float(xml_element.attrib["start"]), + "duration": float(xml_element.attrib.get("dur", "0.0")), } for xml_element in ElementTree.fromstring(plain_data) if xml_element.text is not None diff --git a/youtube_transcript_api/formatters.py b/youtube_transcript_api/formatters.py index 387e565..e693d47 100644 --- a/youtube_transcript_api/formatters.py +++ b/youtube_transcript_api/formatters.py @@ -12,12 +12,16 @@ class Formatter(object): """ def format_transcript(self, transcript, **kwargs): - raise NotImplementedError('A subclass of Formatter must implement ' \ - 'their own .format_transcript() method.') + raise NotImplementedError( + "A subclass of Formatter must implement " + "their own .format_transcript() method." + ) def format_transcripts(self, transcripts, **kwargs): - raise NotImplementedError('A subclass of Formatter must implement ' \ - 'their own .format_transcripts() method.') + raise NotImplementedError( + "A subclass of Formatter must implement " + "their own .format_transcripts() method." + ) class PrettyPrintFormatter(Formatter): @@ -68,7 +72,7 @@ def format_transcript(self, transcript, **kwargs): :return: all transcript text lines separated by newline breaks.' :rtype str """ - return '\n'.join(line['text'] for line in transcript) + return "\n".join(line["text"] for line in transcript) def format_transcripts(self, transcripts, **kwargs): """Converts a list of transcripts into plain text with no timestamps. @@ -77,21 +81,30 @@ def format_transcripts(self, transcripts, **kwargs): :return: all transcript text lines separated by newline breaks.' :rtype str """ - return '\n\n\n'.join([self.format_transcript(transcript, **kwargs) for transcript in transcripts]) + return "\n\n\n".join( + [self.format_transcript(transcript, **kwargs) for transcript in transcripts] + ) + class _TextBasedFormatter(TextFormatter): def _format_timestamp(self, hours, mins, secs, ms): - raise NotImplementedError('A subclass of _TextBasedFormatter must implement ' \ - 'their own .format_timestamp() method.') + raise NotImplementedError( + "A subclass of _TextBasedFormatter must implement " + "their own .format_timestamp() method." + ) def _format_transcript_header(self, lines): - raise NotImplementedError('A subclass of _TextBasedFormatter must implement ' \ - 'their own _format_transcript_header method.') + raise NotImplementedError( + "A subclass of _TextBasedFormatter must implement " + "their own _format_transcript_header method." + ) def _format_transcript_helper(self, i, time_text, line): - raise NotImplementedError('A subclass of _TextBasedFormatter must implement ' \ - 'their own _format_transcript_helper method.') - + raise NotImplementedError( + "A subclass of _TextBasedFormatter must implement " + "their own _format_transcript_helper method." + ) + def _seconds_to_timestamp(self, time): """Helper that converts `time` into a transcript cue timestamp. @@ -109,26 +122,27 @@ def _seconds_to_timestamp(self, time): hours_float, remainder = divmod(time, 3600) mins_float, secs_float = divmod(remainder, 60) hours, mins, secs = int(hours_float), int(mins_float), int(secs_float) - ms = int(round((time - int(time))*1000, 2)) + ms = int(round((time - int(time)) * 1000, 2)) return self._format_timestamp(hours, mins, secs, ms) def format_transcript(self, transcript, **kwargs): """A basic implementation of WEBVTT/SRT formatting. :param transcript: - :reference: + :reference: https://www.w3.org/TR/webvtt1/#introduction-caption https://www.3playmedia.com/blog/create-srt-file/ """ lines = [] for i, line in enumerate(transcript): - end = line['start'] + line['duration'] + end = line["start"] + line["duration"] time_text = "{} --> {}".format( - self._seconds_to_timestamp(line['start']), + self._seconds_to_timestamp(line["start"]), self._seconds_to_timestamp( - transcript[i + 1]['start'] - if i < len(transcript) - 1 and transcript[i + 1]['start'] < end else end - ) + transcript[i + 1]["start"] + if i < len(transcript) - 1 and transcript[i + 1]["start"] < end + else end + ), ) lines.append(self._format_transcript_helper(i, time_text, line)) @@ -138,12 +152,12 @@ def format_transcript(self, transcript, **kwargs): class SRTFormatter(_TextBasedFormatter): def _format_timestamp(self, hours, mins, secs, ms): return "{:02d}:{:02d}:{:02d},{:03d}".format(hours, mins, secs, ms) - + def _format_transcript_header(self, lines): return "\n\n".join(lines) + "\n" def _format_transcript_helper(self, i, time_text, line): - return "{}\n{}\n{}".format(i + 1, time_text, line['text']) + return "{}\n{}\n{}".format(i + 1, time_text, line["text"]) class WebVTTFormatter(_TextBasedFormatter): @@ -154,29 +168,29 @@ def _format_transcript_header(self, lines): return "WEBVTT\n\n" + "\n\n".join(lines) + "\n" def _format_transcript_helper(self, i, time_text, line): - return "{}\n{}".format(time_text, line['text']) + return "{}\n{}".format(time_text, line["text"]) class FormatterLoader(object): TYPES = { - 'json': JSONFormatter, - 'pretty': PrettyPrintFormatter, - 'text': TextFormatter, - 'webvtt': WebVTTFormatter, - 'srt' : SRTFormatter, + "json": JSONFormatter, + "pretty": PrettyPrintFormatter, + "text": TextFormatter, + "webvtt": WebVTTFormatter, + "srt": SRTFormatter, } class UnknownFormatterType(Exception): def __init__(self, formatter_type): super(FormatterLoader.UnknownFormatterType, self).__init__( - 'The format \'{formatter_type}\' is not supported. ' - 'Choose one of the following formats: {supported_formatter_types}'.format( + "The format '{formatter_type}' is not supported. " + "Choose one of the following formats: {supported_formatter_types}".format( formatter_type=formatter_type, - supported_formatter_types=', '.join(FormatterLoader.TYPES.keys()), + supported_formatter_types=", ".join(FormatterLoader.TYPES.keys()), ) ) - def load(self, formatter_type='pretty'): + def load(self, formatter_type="pretty"): """ Loads the Formatter for the given formatter type. diff --git a/youtube_transcript_api/test/test_api.py b/youtube_transcript_api/test/test_api.py index 9b5e732..3d2e48c 100644 --- a/youtube_transcript_api/test/test_api.py +++ b/youtube_transcript_api/test/test_api.py @@ -25,8 +25,9 @@ def load_asset(filename): - filepath = '{dirname}/assets/{filename}'.format( - dirname=os.path.dirname(__file__), filename=filename) + filepath = "{dirname}/assets/{filename}".format( + dirname=os.path.dirname(__file__), filename=filename + ) with open(filepath, mode="rb") as file: return file.read() @@ -37,13 +38,13 @@ def setUp(self): httpretty.enable() httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube.html.static"), ) httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/api/timedtext', - body=load_asset('transcript.xml.static') + "https://www.youtube.com/api/timedtext", + body=load_asset("transcript.xml.static"), ) def tearDown(self): @@ -51,306 +52,362 @@ def tearDown(self): httpretty.disable() def test_get_transcript(self): - transcript = YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8') + transcript = YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8") self.assertEqual( transcript, [ - {'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54}, - {'text': 'this is not the original transcript', 'start': 1.54, 'duration': 4.16}, - {'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239} - ] + {"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54}, + { + "text": "this is not the original transcript", + "start": 1.54, + "duration": 4.16, + }, + { + "text": "just something shorter, I made up for testing", + "start": 5.7, + "duration": 3.239, + }, + ], ) def test_get_transcript_formatted(self): - transcript = YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8', preserve_formatting=True) + transcript = YouTubeTranscriptApi.get_transcript( + "GJLlxj_dtq8", preserve_formatting=True + ) self.assertEqual( transcript, [ - {'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54}, - {'text': 'this is not the original transcript', 'start': 1.54, 'duration': 4.16}, - {'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239} - ] + {"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54}, + { + "text": "this is not the original transcript", + "start": 1.54, + "duration": 4.16, + }, + { + "text": "just something shorter, I made up for testing", + "start": 5.7, + "duration": 3.239, + }, + ], ) def test_list_transcripts(self): - transcript_list = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8') + transcript_list = YouTubeTranscriptApi.list_transcripts("GJLlxj_dtq8") language_codes = {transcript.language_code for transcript in transcript_list} - self.assertEqual(language_codes, {'zh', 'de', 'en', 'hi', 'ja', 'ko', 'es', 'cs', 'en'}) + self.assertEqual( + language_codes, {"zh", "de", "en", "hi", "ja", "ko", "es", "cs", "en"} + ) def test_list_transcripts__find_manually_created(self): - transcript_list = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8') - transcript = transcript_list.find_manually_created_transcript(['cs']) + transcript_list = YouTubeTranscriptApi.list_transcripts("GJLlxj_dtq8") + transcript = transcript_list.find_manually_created_transcript(["cs"]) self.assertFalse(transcript.is_generated) - def test_list_transcripts__find_generated(self): - transcript_list = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8') + transcript_list = YouTubeTranscriptApi.list_transcripts("GJLlxj_dtq8") with self.assertRaises(NoTranscriptFound): - transcript_list.find_generated_transcript(['cs']) + transcript_list.find_generated_transcript(["cs"]) - transcript = transcript_list.find_generated_transcript(['en']) + transcript = transcript_list.find_generated_transcript(["en"]) self.assertTrue(transcript.is_generated) def test_list_transcripts__url_as_video_id(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_transcripts_disabled.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_transcripts_disabled.html.static"), ) with self.assertRaises(InvalidVideoId): - YouTubeTranscriptApi.list_transcripts('https://www.youtube.com/watch?v=GJLlxj_dtq8') - + YouTubeTranscriptApi.list_transcripts( + "https://www.youtube.com/watch?v=GJLlxj_dtq8" + ) def test_list_transcripts__no_translation_languages_provided(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_no_translation_languages.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_no_translation_languages.html.static"), ) - transcript_list = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8') + transcript_list = YouTubeTranscriptApi.list_transcripts("GJLlxj_dtq8") for transcript in transcript_list: self.assertEqual(len(transcript.translation_languages), 0) - def test_translate_transcript(self): - transcript = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8').find_transcript(['en']) + transcript = YouTubeTranscriptApi.list_transcripts( + "GJLlxj_dtq8" + ).find_transcript(["en"]) - translated_transcript = transcript.translate('af') + translated_transcript = transcript.translate("af") - self.assertEqual(translated_transcript.language_code, 'af') - self.assertIn('&tlang=af', translated_transcript._url) + self.assertEqual(translated_transcript.language_code, "af") + self.assertIn("&tlang=af", translated_transcript._url) def test_translate_transcript__translation_language_not_available(self): - transcript = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8').find_transcript(['en']) + transcript = YouTubeTranscriptApi.list_transcripts( + "GJLlxj_dtq8" + ).find_transcript(["en"]) with self.assertRaises(TranslationLanguageNotAvailable): - transcript.translate('xyz') + transcript.translate("xyz") def test_translate_transcript__not_translatable(self): - transcript = YouTubeTranscriptApi.list_transcripts('GJLlxj_dtq8').find_transcript(['en']) + transcript = YouTubeTranscriptApi.list_transcripts( + "GJLlxj_dtq8" + ).find_transcript(["en"]) transcript.translation_languages = [] with self.assertRaises(NotTranslatable): - transcript.translate('af') + transcript.translate("af") def test_get_transcript__correct_language_is_used(self): - YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8', ['de', 'en']) + YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8", ["de", "en"]) query_string = httpretty.last_request().querystring - self.assertIn('lang', query_string) - self.assertEqual(len(query_string['lang']), 1) - self.assertEqual(query_string['lang'][0], 'de') + self.assertIn("lang", query_string) + self.assertEqual(len(query_string["lang"]), 1) + self.assertEqual(query_string["lang"][0], "de") def test_get_transcript__fallback_language_is_used(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_ww1_nl_en.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_ww1_nl_en.html.static"), ) - YouTubeTranscriptApi.get_transcript('F1xioXWb8CY', ['de', 'en']) + YouTubeTranscriptApi.get_transcript("F1xioXWb8CY", ["de", "en"]) query_string = httpretty.last_request().querystring - self.assertIn('lang', query_string) - self.assertEqual(len(query_string['lang']), 1) - self.assertEqual(query_string['lang'][0], 'en') + self.assertIn("lang", query_string) + self.assertEqual(len(query_string["lang"]), 1) + self.assertEqual(query_string["lang"][0], "en") def test_get_transcript__create_consent_cookie_if_needed(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_consent_page.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_consent_page.html.static"), ) - YouTubeTranscriptApi.get_transcript('F1xioXWb8CY') + YouTubeTranscriptApi.get_transcript("F1xioXWb8CY") self.assertEqual(len(httpretty.latest_requests()), 3) for request in httpretty.latest_requests()[1:]: - self.assertEqual(request.headers['cookie'], 'CONSENT=YES+cb.20210328-17-p0.de+FX+119') + self.assertEqual( + request.headers["cookie"], "CONSENT=YES+cb.20210328-17-p0.de+FX+119" + ) def test_get_transcript__exception_if_create_consent_cookie_failed(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_consent_page.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_consent_page.html.static"), ) httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_consent_page.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_consent_page.html.static"), ) with self.assertRaises(FailedToCreateConsentCookie): - YouTubeTranscriptApi.get_transcript('F1xioXWb8CY') + YouTubeTranscriptApi.get_transcript("F1xioXWb8CY") def test_get_transcript__exception_if_consent_cookie_age_invalid(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_consent_page_invalid.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_consent_page_invalid.html.static"), ) with self.assertRaises(FailedToCreateConsentCookie): - YouTubeTranscriptApi.get_transcript('F1xioXWb8CY') + YouTubeTranscriptApi.get_transcript("F1xioXWb8CY") def test_get_transcript__exception_if_video_unavailable(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_video_unavailable.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_video_unavailable.html.static"), ) with self.assertRaises(VideoUnavailable): - YouTubeTranscriptApi.get_transcript('abc') + YouTubeTranscriptApi.get_transcript("abc") def test_get_transcript__exception_if_youtube_request_fails(self): httpretty.register_uri( - httpretty.GET, - 'https://www.youtube.com/watch', - status=500 + httpretty.GET, "https://www.youtube.com/watch", status=500 ) with self.assertRaises(YouTubeRequestFailed): - YouTubeTranscriptApi.get_transcript('abc') + YouTubeTranscriptApi.get_transcript("abc") def test_get_transcript__exception_if_youtube_request_limit_reached(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_too_many_requests.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_too_many_requests.html.static"), ) with self.assertRaises(TooManyRequests): - YouTubeTranscriptApi.get_transcript('abc') + YouTubeTranscriptApi.get_transcript("abc") def test_get_transcript__exception_if_transcripts_disabled(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_transcripts_disabled.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_transcripts_disabled.html.static"), ) with self.assertRaises(TranscriptsDisabled): - YouTubeTranscriptApi.get_transcript('dsMFmonKDD4') + YouTubeTranscriptApi.get_transcript("dsMFmonKDD4") httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_transcripts_disabled2.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_transcripts_disabled2.html.static"), ) with self.assertRaises(TranscriptsDisabled): - YouTubeTranscriptApi.get_transcript('Fjg5lYqvzUs') + YouTubeTranscriptApi.get_transcript("Fjg5lYqvzUs") def test_get_transcript__exception_if_language_unavailable(self): with self.assertRaises(NoTranscriptFound): - YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8', languages=['cz']) + YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8", languages=["cz"]) def test_get_transcript__exception_if_no_transcript_available(self): httpretty.register_uri( httpretty.GET, - 'https://www.youtube.com/watch', - body=load_asset('youtube_no_transcript_available.html.static') + "https://www.youtube.com/watch", + body=load_asset("youtube_no_transcript_available.html.static"), ) with self.assertRaises(NoTranscriptAvailable): - YouTubeTranscriptApi.get_transcript('MwBPvcYFY2E') + YouTubeTranscriptApi.get_transcript("MwBPvcYFY2E") def test_get_transcript__with_proxy(self): - proxies = {'http': '', 'https:': ''} - transcript = YouTubeTranscriptApi.get_transcript( - 'GJLlxj_dtq8', proxies=proxies - ) + proxies = {"http": "", "https:": ""} + transcript = YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8", proxies=proxies) self.assertEqual( transcript, [ - {'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54}, - {'text': 'this is not the original transcript', 'start': 1.54, 'duration': 4.16}, - {'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239} - ] + {"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54}, + { + "text": "this is not the original transcript", + "start": 1.54, + "duration": 4.16, + }, + { + "text": "just something shorter, I made up for testing", + "start": 5.7, + "duration": 3.239, + }, + ], ) - + def test_get_transcript__with_cookies(self): dirname, filename = os.path.split(os.path.abspath(__file__)) - cookies = dirname + '/example_cookies.txt' - transcript = YouTubeTranscriptApi.get_transcript('GJLlxj_dtq8', cookies=cookies) + cookies = dirname + "/example_cookies.txt" + transcript = YouTubeTranscriptApi.get_transcript("GJLlxj_dtq8", cookies=cookies) self.assertEqual( transcript, [ - {'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54}, - {'text': 'this is not the original transcript', 'start': 1.54, 'duration': 4.16}, - {'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239} - ] + {"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54}, + { + "text": "this is not the original transcript", + "start": 1.54, + "duration": 4.16, + }, + { + "text": "just something shorter, I made up for testing", + "start": 5.7, + "duration": 3.239, + }, + ], ) def test_get_transcript__assertionerror_if_input_not_string(self): with self.assertRaises(AssertionError): - YouTubeTranscriptApi.get_transcript(['video_id_1', 'video_id_2']) + YouTubeTranscriptApi.get_transcript(["video_id_1", "video_id_2"]) def test_get_transcripts__assertionerror_if_input_not_list(self): with self.assertRaises(AssertionError): - YouTubeTranscriptApi.get_transcripts('video_id_1') + YouTubeTranscriptApi.get_transcripts("video_id_1") - @patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript') + @patch("youtube_transcript_api.YouTubeTranscriptApi.get_transcript") def test_get_transcripts(self, mock_get_transcript): - video_id_1 = 'video_id_1' - video_id_2 = 'video_id_2' - languages = ['de', 'en'] + video_id_1 = "video_id_1" + video_id_2 = "video_id_2" + languages = ["de", "en"] - YouTubeTranscriptApi.get_transcripts([video_id_1, video_id_2], languages=languages) + YouTubeTranscriptApi.get_transcripts( + [video_id_1, video_id_2], languages=languages + ) mock_get_transcript.assert_any_call(video_id_1, languages, None, None, False) mock_get_transcript.assert_any_call(video_id_2, languages, None, None, False) self.assertEqual(mock_get_transcript.call_count, 2) - @patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript', side_effect=Exception('Error')) + @patch( + "youtube_transcript_api.YouTubeTranscriptApi.get_transcript", + side_effect=Exception("Error"), + ) def test_get_transcripts__stop_on_error(self, mock_get_transcript): with self.assertRaises(Exception): - YouTubeTranscriptApi.get_transcripts(['video_id_1', 'video_id_2']) + YouTubeTranscriptApi.get_transcripts(["video_id_1", "video_id_2"]) - @patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript', side_effect=Exception('Error')) + @patch( + "youtube_transcript_api.YouTubeTranscriptApi.get_transcript", + side_effect=Exception("Error"), + ) def test_get_transcripts__continue_on_error(self, mock_get_transcript): - video_id_1 = 'video_id_1' - video_id_2 = 'video_id_2' + video_id_1 = "video_id_1" + video_id_2 = "video_id_2" + + YouTubeTranscriptApi.get_transcripts( + ["video_id_1", "video_id_2"], continue_after_error=True + ) - YouTubeTranscriptApi.get_transcripts(['video_id_1', 'video_id_2'], continue_after_error=True) + mock_get_transcript.assert_any_call(video_id_1, ("en",), None, None, False) + mock_get_transcript.assert_any_call(video_id_2, ("en",), None, None, False) - mock_get_transcript.assert_any_call(video_id_1, ('en',), None, None, False) - mock_get_transcript.assert_any_call(video_id_2, ('en',), None, None, False) - - @patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript') + @patch("youtube_transcript_api.YouTubeTranscriptApi.get_transcript") def test_get_transcripts__with_cookies(self, mock_get_transcript): - cookies = '/example_cookies.txt' - YouTubeTranscriptApi.get_transcripts(['GJLlxj_dtq8'], cookies=cookies) - mock_get_transcript.assert_any_call('GJLlxj_dtq8', ('en',), None, cookies, False) + cookies = "/example_cookies.txt" + YouTubeTranscriptApi.get_transcripts(["GJLlxj_dtq8"], cookies=cookies) + mock_get_transcript.assert_any_call( + "GJLlxj_dtq8", ("en",), None, cookies, False + ) - @patch('youtube_transcript_api.YouTubeTranscriptApi.get_transcript') + @patch("youtube_transcript_api.YouTubeTranscriptApi.get_transcript") def test_get_transcripts__with_proxies(self, mock_get_transcript): - proxies = {'http': '', 'https:': ''} - YouTubeTranscriptApi.get_transcripts(['GJLlxj_dtq8'], proxies=proxies) - mock_get_transcript.assert_any_call('GJLlxj_dtq8', ('en',), proxies, None, False) + proxies = {"http": "", "https:": ""} + YouTubeTranscriptApi.get_transcripts(["GJLlxj_dtq8"], proxies=proxies) + mock_get_transcript.assert_any_call( + "GJLlxj_dtq8", ("en",), proxies, None, False + ) def test_load_cookies(self): dirname, filename = os.path.split(os.path.abspath(__file__)) - cookies = dirname + '/example_cookies.txt' - session_cookies = YouTubeTranscriptApi._load_cookies(cookies, 'GJLlxj_dtq8') - self.assertEqual({'TEST_FIELD': 'TEST_VALUE'}, requests.utils.dict_from_cookiejar(session_cookies)) + cookies = dirname + "/example_cookies.txt" + session_cookies = YouTubeTranscriptApi._load_cookies(cookies, "GJLlxj_dtq8") + self.assertEqual( + {"TEST_FIELD": "TEST_VALUE"}, + requests.utils.dict_from_cookiejar(session_cookies), + ) def test_load_cookies__bad_file_path(self): - bad_cookies = 'nonexistent_cookies.txt' + bad_cookies = "nonexistent_cookies.txt" with self.assertRaises(CookiePathInvalid): - YouTubeTranscriptApi._load_cookies(bad_cookies, 'GJLlxj_dtq8') + YouTubeTranscriptApi._load_cookies(bad_cookies, "GJLlxj_dtq8") def test_load_cookies__no_valid_cookies(self): dirname, filename = os.path.split(os.path.abspath(__file__)) - expired_cookies = dirname + '/expired_example_cookies.txt' + expired_cookies = dirname + "/expired_example_cookies.txt" with self.assertRaises(CookiesInvalid): - YouTubeTranscriptApi._load_cookies(expired_cookies, 'GJLlxj_dtq8') + YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") diff --git a/youtube_transcript_api/test/test_cli.py b/youtube_transcript_api/test/test_cli.py index 26ffabc..623d4a4 100644 --- a/youtube_transcript_api/test/test_cli.py +++ b/youtube_transcript_api/test/test_cli.py @@ -10,211 +10,269 @@ class TestYouTubeTranscriptCli(TestCase): def setUp(self): self.transcript_mock = MagicMock() - self.transcript_mock.fetch = MagicMock(return_value=[ - {'text': 'Hey, this is just a test', 'start': 0.0, 'duration': 1.54}, - {'text': 'this is not the original transcript', 'start': 1.54, 'duration': 4.16}, - {'text': 'just something shorter, I made up for testing', 'start': 5.7, 'duration': 3.239} - ]) + self.transcript_mock.fetch = MagicMock( + return_value=[ + {"text": "Hey, this is just a test", "start": 0.0, "duration": 1.54}, + { + "text": "this is not the original transcript", + "start": 1.54, + "duration": 4.16, + }, + { + "text": "just something shorter, I made up for testing", + "start": 5.7, + "duration": 3.239, + }, + ] + ) self.transcript_mock.translate = MagicMock(return_value=self.transcript_mock) self.transcript_list_mock = MagicMock() - self.transcript_list_mock.find_generated_transcript = MagicMock(return_value=self.transcript_mock) - self.transcript_list_mock.find_manually_created_transcript = MagicMock(return_value=self.transcript_mock) - self.transcript_list_mock.find_transcript = MagicMock(return_value=self.transcript_mock) + self.transcript_list_mock.find_generated_transcript = MagicMock( + return_value=self.transcript_mock + ) + self.transcript_list_mock.find_manually_created_transcript = MagicMock( + return_value=self.transcript_mock + ) + self.transcript_list_mock.find_transcript = MagicMock( + return_value=self.transcript_mock + ) - YouTubeTranscriptApi.list_transcripts = MagicMock(return_value=self.transcript_list_mock) + YouTubeTranscriptApi.list_transcripts = MagicMock( + return_value=self.transcript_list_mock + ) def test_argument_parsing(self): - parsed_args = YouTubeTranscriptCli('v1 v2 --format json --languages de en'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'json') - self.assertEqual(parsed_args.languages, ['de', 'en']) - self.assertEqual(parsed_args.http_proxy, '') - self.assertEqual(parsed_args.https_proxy, '') + parsed_args = YouTubeTranscriptCli( + "v1 v2 --format json --languages de en".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.http_proxy, "") + self.assertEqual(parsed_args.https_proxy, "") - parsed_args = YouTubeTranscriptCli('v1 v2 --languages de en --format json'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'json') - self.assertEqual(parsed_args.languages, ['de', 'en']) - self.assertEqual(parsed_args.http_proxy, '') - self.assertEqual(parsed_args.https_proxy, '') + parsed_args = YouTubeTranscriptCli( + "v1 v2 --languages de en --format json".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.http_proxy, "") + self.assertEqual(parsed_args.https_proxy, "") - parsed_args = YouTubeTranscriptCli(' --format json v1 v2 --languages de en'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'json') - self.assertEqual(parsed_args.languages, ['de', 'en']) - self.assertEqual(parsed_args.http_proxy, '') - self.assertEqual(parsed_args.https_proxy, '') + parsed_args = YouTubeTranscriptCli( + " --format json v1 v2 --languages de en".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.http_proxy, "") + self.assertEqual(parsed_args.https_proxy, "") parsed_args = YouTubeTranscriptCli( - 'v1 v2 --languages de en --format json ' - '--http-proxy http://user:pass@domain:port ' - '--https-proxy https://user:pass@domain:port'.split() + "v1 v2 --languages de en --format json " + "--http-proxy http://user:pass@domain:port " + "--https-proxy https://user:pass@domain:port".split() )._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'json') - self.assertEqual(parsed_args.languages, ['de', 'en']) - self.assertEqual(parsed_args.http_proxy, 'http://user:pass@domain:port') - self.assertEqual(parsed_args.https_proxy, 'https://user:pass@domain:port') + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port") + self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port") parsed_args = YouTubeTranscriptCli( - 'v1 v2 --languages de en --format json --http-proxy http://user:pass@domain:port'.split() + "v1 v2 --languages de en --format json --http-proxy http://user:pass@domain:port".split() )._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'json') - self.assertEqual(parsed_args.languages, ['de', 'en']) - self.assertEqual(parsed_args.http_proxy, 'http://user:pass@domain:port') - self.assertEqual(parsed_args.https_proxy, '') + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port") + self.assertEqual(parsed_args.https_proxy, "") parsed_args = YouTubeTranscriptCli( - 'v1 v2 --languages de en --format json --https-proxy https://user:pass@domain:port'.split() + "v1 v2 --languages de en --format json --https-proxy https://user:pass@domain:port".split() )._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'json') - self.assertEqual(parsed_args.languages, ['de', 'en']) - self.assertEqual(parsed_args.https_proxy, 'https://user:pass@domain:port') - self.assertEqual(parsed_args.http_proxy, '') + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port") + self.assertEqual(parsed_args.http_proxy, "") def test_argument_parsing__only_video_ids(self): - parsed_args = YouTubeTranscriptCli('v1 v2'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'pretty') - self.assertEqual(parsed_args.languages, ['en']) + parsed_args = YouTubeTranscriptCli("v1 v2".split())._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "pretty") + self.assertEqual(parsed_args.languages, ["en"]) def test_argument_parsing__video_ids_starting_with_dash(self): - parsed_args = YouTubeTranscriptCli('\-v1 \-\-v2 \--v3'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['-v1', '--v2', '--v3']) - self.assertEqual(parsed_args.format, 'pretty') - self.assertEqual(parsed_args.languages, ['en']) + parsed_args = YouTubeTranscriptCli("\-v1 \-\-v2 \--v3".split())._parse_args() + self.assertEqual(parsed_args.video_ids, ["-v1", "--v2", "--v3"]) + self.assertEqual(parsed_args.format, "pretty") + self.assertEqual(parsed_args.languages, ["en"]) def test_argument_parsing__fail_without_video_ids(self): with self.assertRaises(SystemExit): - YouTubeTranscriptCli('--format json'.split())._parse_args() + YouTubeTranscriptCli("--format json".split())._parse_args() def test_argument_parsing__json(self): - parsed_args = YouTubeTranscriptCli('v1 v2 --format json'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'json') - self.assertEqual(parsed_args.languages, ['en']) + parsed_args = YouTubeTranscriptCli("v1 v2 --format json".split())._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["en"]) - parsed_args = YouTubeTranscriptCli('--format json v1 v2'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'json') - self.assertEqual(parsed_args.languages, ['en']) + parsed_args = YouTubeTranscriptCli("--format json v1 v2".split())._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "json") + self.assertEqual(parsed_args.languages, ["en"]) def test_argument_parsing__languages(self): - parsed_args = YouTubeTranscriptCli('v1 v2 --languages de en'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'pretty') - self.assertEqual(parsed_args.languages, ['de', 'en']) - - def test_argument_parsing__proxies(self): parsed_args = YouTubeTranscriptCli( - 'v1 v2 --http-proxy http://user:pass@domain:port'.split() + "v1 v2 --languages de en".split() )._parse_args() - self.assertEqual(parsed_args.http_proxy, 'http://user:pass@domain:port') + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "pretty") + self.assertEqual(parsed_args.languages, ["de", "en"]) + def test_argument_parsing__proxies(self): parsed_args = YouTubeTranscriptCli( - 'v1 v2 --https-proxy https://user:pass@domain:port'.split() + "v1 v2 --http-proxy http://user:pass@domain:port".split() )._parse_args() - self.assertEqual(parsed_args.https_proxy, 'https://user:pass@domain:port') + self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port") parsed_args = YouTubeTranscriptCli( - 'v1 v2 --http-proxy http://user:pass@domain:port --https-proxy https://user:pass@domain:port'.split() + "v1 v2 --https-proxy https://user:pass@domain:port".split() )._parse_args() - self.assertEqual(parsed_args.http_proxy, 'http://user:pass@domain:port') - self.assertEqual(parsed_args.https_proxy, 'https://user:pass@domain:port') + self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port") parsed_args = YouTubeTranscriptCli( - 'v1 v2'.split() + "v1 v2 --http-proxy http://user:pass@domain:port --https-proxy https://user:pass@domain:port".split() )._parse_args() - self.assertEqual(parsed_args.http_proxy, '') - self.assertEqual(parsed_args.https_proxy, '') + self.assertEqual(parsed_args.http_proxy, "http://user:pass@domain:port") + self.assertEqual(parsed_args.https_proxy, "https://user:pass@domain:port") + + parsed_args = YouTubeTranscriptCli("v1 v2".split())._parse_args() + self.assertEqual(parsed_args.http_proxy, "") + self.assertEqual(parsed_args.https_proxy, "") def test_argument_parsing__list_transcripts(self): - parsed_args = YouTubeTranscriptCli('--list-transcripts v1 v2'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) + parsed_args = YouTubeTranscriptCli( + "--list-transcripts v1 v2".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) self.assertTrue(parsed_args.list_transcripts) - parsed_args = YouTubeTranscriptCli('v1 v2 --list-transcripts'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) + parsed_args = YouTubeTranscriptCli( + "v1 v2 --list-transcripts".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) self.assertTrue(parsed_args.list_transcripts) def test_argument_parsing__translate(self): - parsed_args = YouTubeTranscriptCli('v1 v2 --languages de en --translate cz'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'pretty') - self.assertEqual(parsed_args.languages, ['de', 'en']) - self.assertEqual(parsed_args.translate, 'cz') - - parsed_args = YouTubeTranscriptCli('v1 v2 --translate cz --languages de en'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) - self.assertEqual(parsed_args.format, 'pretty') - self.assertEqual(parsed_args.languages, ['de', 'en']) - self.assertEqual(parsed_args.translate, 'cz') + parsed_args = YouTubeTranscriptCli( + "v1 v2 --languages de en --translate cz".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "pretty") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.translate, "cz") + + parsed_args = YouTubeTranscriptCli( + "v1 v2 --translate cz --languages de en".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) + self.assertEqual(parsed_args.format, "pretty") + self.assertEqual(parsed_args.languages, ["de", "en"]) + self.assertEqual(parsed_args.translate, "cz") def test_argument_parsing__manually_or_generated(self): - parsed_args = YouTubeTranscriptCli('v1 v2 --exclude-manually-created'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) + parsed_args = YouTubeTranscriptCli( + "v1 v2 --exclude-manually-created".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) self.assertTrue(parsed_args.exclude_manually_created) self.assertFalse(parsed_args.exclude_generated) - parsed_args = YouTubeTranscriptCli('v1 v2 --exclude-generated'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) + parsed_args = YouTubeTranscriptCli( + "v1 v2 --exclude-generated".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) self.assertFalse(parsed_args.exclude_manually_created) self.assertTrue(parsed_args.exclude_generated) - parsed_args = YouTubeTranscriptCli('v1 v2 --exclude-manually-created --exclude-generated'.split())._parse_args() - self.assertEqual(parsed_args.video_ids, ['v1', 'v2']) + parsed_args = YouTubeTranscriptCli( + "v1 v2 --exclude-manually-created --exclude-generated".split() + )._parse_args() + self.assertEqual(parsed_args.video_ids, ["v1", "v2"]) self.assertTrue(parsed_args.exclude_manually_created) self.assertTrue(parsed_args.exclude_generated) def test_run(self): - YouTubeTranscriptCli('v1 v2 --languages de en'.split()).run() + YouTubeTranscriptCli("v1 v2 --languages de en".split()).run() - YouTubeTranscriptApi.list_transcripts.assert_any_call('v1', proxies=None, cookies=None) - YouTubeTranscriptApi.list_transcripts.assert_any_call('v2', proxies=None, cookies=None) + YouTubeTranscriptApi.list_transcripts.assert_any_call( + "v1", proxies=None, cookies=None + ) + YouTubeTranscriptApi.list_transcripts.assert_any_call( + "v2", proxies=None, cookies=None + ) - self.transcript_list_mock.find_transcript.assert_any_call(['de', 'en']) + self.transcript_list_mock.find_transcript.assert_any_call(["de", "en"]) def test_run__failing_transcripts(self): - YouTubeTranscriptApi.list_transcripts = MagicMock(side_effect=VideoUnavailable('video_id')) + YouTubeTranscriptApi.list_transcripts = MagicMock( + side_effect=VideoUnavailable("video_id") + ) - output = YouTubeTranscriptCli('v1 --languages de en'.split()).run() + output = YouTubeTranscriptCli("v1 --languages de en".split()).run() - self.assertEqual(output, str(VideoUnavailable('video_id'))) + self.assertEqual(output, str(VideoUnavailable("video_id"))) def test_run__exclude_generated(self): - YouTubeTranscriptCli('v1 v2 --languages de en --exclude-generated'.split()).run() + YouTubeTranscriptCli( + "v1 v2 --languages de en --exclude-generated".split() + ).run() - self.transcript_list_mock.find_manually_created_transcript.assert_any_call(['de', 'en']) + self.transcript_list_mock.find_manually_created_transcript.assert_any_call( + ["de", "en"] + ) def test_run__exclude_manually_created(self): - YouTubeTranscriptCli('v1 v2 --languages de en --exclude-manually-created'.split()).run() + YouTubeTranscriptCli( + "v1 v2 --languages de en --exclude-manually-created".split() + ).run() - self.transcript_list_mock.find_generated_transcript.assert_any_call(['de', 'en']) + self.transcript_list_mock.find_generated_transcript.assert_any_call( + ["de", "en"] + ) def test_run__exclude_manually_created_and_generated(self): self.assertEqual( YouTubeTranscriptCli( - 'v1 v2 --languages de en --exclude-manually-created --exclude-generated'.split() + "v1 v2 --languages de en --exclude-manually-created --exclude-generated".split() ).run(), - '' + "", ) def test_run__translate(self): - YouTubeTranscriptCli('v1 v2 --languages de en --translate cz'.split()).run(), + YouTubeTranscriptCli("v1 v2 --languages de en --translate cz".split()).run(), - self.transcript_mock.translate.assert_any_call('cz') + self.transcript_mock.translate.assert_any_call("cz") def test_run__list_transcripts(self): - YouTubeTranscriptCli('--list-transcripts v1 v2'.split()).run() + YouTubeTranscriptCli("--list-transcripts v1 v2".split()).run() - YouTubeTranscriptApi.list_transcripts.assert_any_call('v1', proxies=None, cookies=None) - YouTubeTranscriptApi.list_transcripts.assert_any_call('v2', proxies=None, cookies=None) + YouTubeTranscriptApi.list_transcripts.assert_any_call( + "v1", proxies=None, cookies=None + ) + YouTubeTranscriptApi.list_transcripts.assert_any_call( + "v2", proxies=None, cookies=None + ) def test_run__json_output(self): - output = YouTubeTranscriptCli('v1 v2 --languages de en --format json'.split()).run() + output = YouTubeTranscriptCli( + "v1 v2 --languages de en --format json".split() + ).run() # will fail if output is not valid json json.loads(output) @@ -222,31 +280,37 @@ def test_run__json_output(self): def test_run__proxies(self): YouTubeTranscriptCli( ( - 'v1 v2 --languages de en ' - '--http-proxy http://user:pass@domain:port ' - '--https-proxy https://user:pass@domain:port' + "v1 v2 --languages de en " + "--http-proxy http://user:pass@domain:port " + "--https-proxy https://user:pass@domain:port" ).split() ).run() YouTubeTranscriptApi.list_transcripts.assert_any_call( - 'v1', - proxies={'http': 'http://user:pass@domain:port', 'https': 'https://user:pass@domain:port'}, - cookies= None + "v1", + proxies={ + "http": "http://user:pass@domain:port", + "https": "https://user:pass@domain:port", + }, + cookies=None, ) YouTubeTranscriptApi.list_transcripts.assert_any_call( - 'v2', - proxies={'http': 'http://user:pass@domain:port', 'https': 'https://user:pass@domain:port'}, - cookies=None + "v2", + proxies={ + "http": "http://user:pass@domain:port", + "https": "https://user:pass@domain:port", + }, + cookies=None, ) def test_run__cookies(self): YouTubeTranscriptCli( - ( - 'v1 v2 --languages de en ' - '--cookies blahblah.txt' - ).split() + ("v1 v2 --languages de en " "--cookies blahblah.txt").split() ).run() - YouTubeTranscriptApi.list_transcripts.assert_any_call('v1', proxies=None, cookies='blahblah.txt') - YouTubeTranscriptApi.list_transcripts.assert_any_call('v2', proxies=None, cookies='blahblah.txt') - + YouTubeTranscriptApi.list_transcripts.assert_any_call( + "v1", proxies=None, cookies="blahblah.txt" + ) + YouTubeTranscriptApi.list_transcripts.assert_any_call( + "v2", proxies=None, cookies="blahblah.txt" + ) diff --git a/youtube_transcript_api/test/test_formatters.py b/youtube_transcript_api/test/test_formatters.py index b0b3ba2..7eda79a 100644 --- a/youtube_transcript_api/test/test_formatters.py +++ b/youtube_transcript_api/test/test_formatters.py @@ -10,16 +10,17 @@ TextFormatter, SRTFormatter, WebVTTFormatter, - PrettyPrintFormatter, FormatterLoader + PrettyPrintFormatter, + FormatterLoader, ) class TestFormatters(TestCase): def setUp(self): self.transcript = [ - {'text': 'Test line 1', 'start': 0.0, 'duration': 1.50}, - {'text': 'line between', 'start': 1.5, 'duration': 2.0}, - {'text': 'testing the end line', 'start': 2.5, 'duration': 3.25} + {"text": "Test line 1", "start": 0.0, "duration": 1.50}, + {"text": "line between", "start": 1.5, "duration": 2.0}, + {"text": "testing the end line", "start": 2.5, "duration": 3.25}, ] self.transcripts = [self.transcript, self.transcript] @@ -31,27 +32,27 @@ def test_base_formatter_format_call(self): def test_srt_formatter_starting(self): content = SRTFormatter().format_transcript(self.transcript) - lines = content.split('\n') + lines = content.split("\n") # test starting lines self.assertEqual(lines[0], "1") self.assertEqual(lines[1], "00:00:00,000 --> 00:00:01,500") - + def test_srt_formatter_middle(self): content = SRTFormatter().format_transcript(self.transcript) - lines = content.split('\n') + lines = content.split("\n") # test middle lines self.assertEqual(lines[4], "2") self.assertEqual(lines[5], "00:00:01,500 --> 00:00:02,500") - self.assertEqual(lines[6], self.transcript[1]['text']) + self.assertEqual(lines[6], self.transcript[1]["text"]) def test_srt_formatter_ending(self): content = SRTFormatter().format_transcript(self.transcript) - lines = content.split('\n') + lines = content.split("\n") # test ending lines - self.assertEqual(lines[-2], self.transcript[-1]['text']) + self.assertEqual(lines[-2], self.transcript[-1]["text"]) self.assertEqual(lines[-1], "") def test_srt_formatter_many(self): @@ -59,22 +60,25 @@ def test_srt_formatter_many(self): content = formatter.format_transcripts(self.transcripts) formatted_single_transcript = formatter.format_transcript(self.transcript) - self.assertEqual(content, formatted_single_transcript + '\n\n\n' + formatted_single_transcript) + self.assertEqual( + content, + formatted_single_transcript + "\n\n\n" + formatted_single_transcript, + ) def test_webvtt_formatter_starting(self): content = WebVTTFormatter().format_transcript(self.transcript) - lines = content.split('\n') + lines = content.split("\n") # test starting lines self.assertEqual(lines[0], "WEBVTT") self.assertEqual(lines[1], "") - + def test_webvtt_formatter_ending(self): content = WebVTTFormatter().format_transcript(self.transcript) - lines = content.split('\n') + lines = content.split("\n") # test ending lines - self.assertEqual(lines[-2], self.transcript[-1]['text']) + self.assertEqual(lines[-2], self.transcript[-1]["text"]) self.assertEqual(lines[-1], "") def test_webvtt_formatter_many(self): @@ -82,7 +86,10 @@ def test_webvtt_formatter_many(self): content = formatter.format_transcripts(self.transcripts) formatted_single_transcript = formatter.format_transcript(self.transcript) - self.assertEqual(content, formatted_single_transcript + '\n\n\n' + formatted_single_transcript) + self.assertEqual( + content, + formatted_single_transcript + "\n\n\n" + formatted_single_transcript, + ) def test_pretty_print_formatter(self): content = PrettyPrintFormatter().format_transcript(self.transcript) @@ -106,7 +113,7 @@ def test_json_formatter_many(self): def test_text_formatter(self): content = TextFormatter().format_transcript(self.transcript) - lines = content.split('\n') + lines = content.split("\n") self.assertEqual(lines[0], self.transcript[0]["text"]) self.assertEqual(lines[-1], self.transcript[-1]["text"]) @@ -116,11 +123,14 @@ def test_text_formatter_many(self): content = formatter.format_transcripts(self.transcripts) formatted_single_transcript = formatter.format_transcript(self.transcript) - self.assertEqual(content, formatted_single_transcript + '\n\n\n' + formatted_single_transcript) + self.assertEqual( + content, + formatted_single_transcript + "\n\n\n" + formatted_single_transcript, + ) def test_formatter_loader(self): loader = FormatterLoader() - formatter = loader.load('json') + formatter = loader.load("json") self.assertTrue(isinstance(formatter, JSONFormatter)) @@ -132,4 +142,4 @@ def test_formatter_loader__default_formatter(self): def test_formatter_loader__unknown_format(self): with self.assertRaises(FormatterLoader.UnknownFormatterType): - FormatterLoader().load('png') + FormatterLoader().load("png") From b9d9f238979d66f6465b9ca46c229d15bb74458b Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Thu, 26 Sep 2024 18:05:23 +0200 Subject: [PATCH 03/31] added ruff linter --- poetry.lock | 29 +++++++++++++++++++++++++- pyproject.toml | 2 ++ youtube_transcript_api/__init__.py | 1 + youtube_transcript_api/_transcripts.py | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index b3fc681..2e94db6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -469,6 +469,33 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "ruff" +version = "0.6.8" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"}, + {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"}, + {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"}, + {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"}, + {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"}, + {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"}, + {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -511,4 +538,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "4c2e7d294773ea148b69f961053a9469630c48b88248903ead43e41a2838ff94" +content-hash = "5a0e6785d468ab04c036285650de9dc185c8ebca0eba66a1c53f7962ac336e1a" diff --git a/pyproject.toml b/pyproject.toml index 5720af7..0e6b614 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" test = "pytest youtube_transcript_api" coverage.shell = "pytest youtube_transcript_api && coverage report -m" format = "black youtube_transcript_api" +lint = "ruff check youtube_transcript_api" [tool.poetry.dependencies] python = ">=3.8,<3.13" @@ -53,6 +54,7 @@ mock = "^5.1.0" httpretty = "^1.1.4" coveralls = "^4.0.1" black = "^24.8.0" +ruff = "^0.6.8" [tool.coverage.run] source = ["youtube_transcript_api"] diff --git a/youtube_transcript_api/__init__.py b/youtube_transcript_api/__init__.py index 7f703f4..2c338d8 100644 --- a/youtube_transcript_api/__init__.py +++ b/youtube_transcript_api/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 from ._api import YouTubeTranscriptApi from ._transcripts import TranscriptList, Transcript from ._errors import ( diff --git a/youtube_transcript_api/_transcripts.py b/youtube_transcript_api/_transcripts.py index 7ce4d2e..f93f717 100644 --- a/youtube_transcript_api/_transcripts.py +++ b/youtube_transcript_api/_transcripts.py @@ -2,6 +2,7 @@ # This can only be tested by using different python versions, therefore it is not covered by coverage.py if sys.version_info.major == 2: # pragma: no cover + # ruff: noqa: F821 reload(sys) sys.setdefaultencoding("utf-8") From d35cb58cce3cbd0a93f08275bfb17ba624c70980 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 10:47:03 +0100 Subject: [PATCH 04/31] changed to use ruff format for formatting instead of black --- pyproject.toml | 5 ++--- youtube_transcript_api/test/test_cli.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0e6b614..ff53bf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" [tool.poe.tasks] test = "pytest youtube_transcript_api" -coverage.shell = "pytest youtube_transcript_api && coverage report -m" -format = "black youtube_transcript_api" +coverage.shell = "coverage run -m unittest discover && coverage report -m" +format = "ruff format youtube_transcript_api" lint = "ruff check youtube_transcript_api" [tool.poetry.dependencies] @@ -53,7 +53,6 @@ coverage = "^7.6.1" mock = "^5.1.0" httpretty = "^1.1.4" coveralls = "^4.0.1" -black = "^24.8.0" ruff = "^0.6.8" [tool.coverage.run] diff --git a/youtube_transcript_api/test/test_cli.py b/youtube_transcript_api/test/test_cli.py index 623d4a4..dd21b39 100644 --- a/youtube_transcript_api/test/test_cli.py +++ b/youtube_transcript_api/test/test_cli.py @@ -255,7 +255,7 @@ def test_run__exclude_manually_created_and_generated(self): ) def test_run__translate(self): - YouTubeTranscriptCli("v1 v2 --languages de en --translate cz".split()).run(), + (YouTubeTranscriptCli("v1 v2 --languages de en --translate cz".split()).run(),) self.transcript_mock.translate.assert_any_call("cz") From 3c1acf3d9cd0b4642399034eb1801e4beededc76 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 11:22:21 +0100 Subject: [PATCH 05/31] added GitHub Action CI for formatting, linting and testing --- .github/workflows/ci.yml | 46 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 10 +++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..65d0a18 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [ "master" ] + pull_request: + +jobs: + static-checks: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: 3.9 + - name: Install dependencies + run: | + pip install poetry poe + poetry install --only dev + - name: Format + run: poe ci-format + - name: Lint + run: poe lint + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install poetry poe + poetry install --with test + - name: Run tests + run: | + poe ci-test diff --git a/pyproject.toml b/pyproject.toml index ff53bf9..069c50f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,20 +39,30 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" [tool.poe.tasks] test = "pytest youtube_transcript_api" +ci-test.shell = "coverage run -m unittest discover && coverage report -m --fail-under=100" coverage.shell = "coverage run -m unittest discover && coverage report -m" format = "ruff format youtube_transcript_api" +ci-format = "ruff format youtube_transcript_api --check" lint = "ruff check youtube_transcript_api" [tool.poetry.dependencies] python = ">=3.8,<3.13" requests = "*" +[tool.poetry.group.test] +optional = true + [tool.poetry.group.test.dependencies] pytest = "^8.3.3" coverage = "^7.6.1" mock = "^5.1.0" httpretty = "^1.1.4" coveralls = "^4.0.1" + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] ruff = "^0.6.8" [tool.coverage.run] From d7a974a4d9ebe3f046d3dd55815d252b67d8ce2b Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 11:33:40 +0100 Subject: [PATCH 06/31] fixed poe install in ci.yml --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65d0a18..beddad9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,14 +17,14 @@ jobs: python-version: 3.9 - name: Install dependencies run: | - pip install poetry poe + pip install poetry poethepoet poetry install --only dev - name: Format run: poe ci-format - name: Lint run: poe lint - test: + test-coverage: runs-on: ubuntu-latest strategy: fail-fast: false @@ -39,7 +39,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install poetry poe + pip install poetry poethepoet poetry install --with test - name: Run tests run: | From 8cf7470fb769c83caa598fb16d1543f74e723222 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 11:37:08 +0100 Subject: [PATCH 07/31] upgraded setup-python action to v5 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index beddad9..ab191bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 3f5c0bcfec0e756797efedcaf0835b2ac4a3d543 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 11:38:58 +0100 Subject: [PATCH 08/31] updated lockfile --- poetry.lock | 358 +++++++++++++++++++--------------------------------- 1 file changed, 132 insertions(+), 226 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2e94db6..0c96bac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,51 +1,5 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. -[[package]] -name = "black" -version = "24.8.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, - {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, - {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, - {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, - {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, - {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, - {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, - {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, - {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, - {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, - {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, - {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, - {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, - {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, - {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, - {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, - {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, - {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, - {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, - {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, - {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, - {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" version = "2024.8.30" @@ -59,117 +13,118 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.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.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -362,55 +317,17 @@ build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest", "pytest-cov"] -[[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 = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - [[package]] name = "pluggy" version = "1.5.0" @@ -471,51 +388,40 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.6.8" +version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"}, - {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"}, - {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"}, - {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"}, - {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"}, - {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"}, - {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"}, - {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"}, - {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"}, - {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"}, - {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"}, + {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, + {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, + {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, + {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, + {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, + {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, + {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, ] [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -538,4 +444,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "5a0e6785d468ab04c036285650de9dc185c8ebca0eba66a1c53f7962ac336e1a" +content-hash = "ef642ce9df6240dee52ca62d50fcb18bf0e99a717067a45a101fdf888e06458c" From e5bd9ae825207d95a7fae3f7872b8fa114d61372 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 12:04:38 +0100 Subject: [PATCH 09/31] added coveralls integration --- .github/workflows/ci.yml | 21 +++++++++++++++++++-- poetry.lock | 36 ++---------------------------------- pyproject.toml | 5 ++--- 3 files changed, 23 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab191bc..afd4ac5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,12 +24,12 @@ jobs: - name: Lint run: poe lint - test-coverage: + test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 @@ -44,3 +44,20 @@ jobs: - name: Run tests run: | poe ci-test + - name: Report intermediate coverage report + uses: coverallsapp/github-action@v2 + with: + flag-name: run-python-${{ matrix.python-version }} + parallel: true + +finish: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Finalize coverage report + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13,run-python-3.14" + diff --git a/poetry.lock b/poetry.lock index 0c96bac..d0d5582 100644 --- a/poetry.lock +++ b/poetry.lock @@ -217,41 +217,9 @@ files = [ {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli"] -[[package]] -name = "coveralls" -version = "4.0.1" -description = "Show coverage stats online via coveralls.io" -optional = false -python-versions = "<3.13,>=3.8" -files = [ - {file = "coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809"}, - {file = "coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69"}, -] - -[package.dependencies] -coverage = {version = ">=5.0,<6.0.dev0 || >6.1,<6.1.1 || >6.1.1,<8.0", extras = ["toml"]} -docopt = ">=0.6.1,<0.7.0" -requests = ">=1.0.0,<3.0.0" - -[package.extras] -yaml = ["pyyaml (>=3.10,<7.0)"] - -[[package]] -name = "docopt" -version = "0.6.2" -description = "Pythonic argument parser, that will make you smile" -optional = false -python-versions = "*" -files = [ - {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, -] - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -443,5 +411,5 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" -python-versions = ">=3.8,<3.13" -content-hash = "ef642ce9df6240dee52ca62d50fcb18bf0e99a717067a45a101fdf888e06458c" +python-versions = ">=3.8,<=3.14" +content-hash = "1fc5fdbba555a4380ff80fda6988a95ecbaa342ca863cf19f8fc8e3df0430ff7" diff --git a/pyproject.toml b/pyproject.toml index 069c50f..03860b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,14 +39,14 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" [tool.poe.tasks] test = "pytest youtube_transcript_api" -ci-test.shell = "coverage run -m unittest discover && coverage report -m --fail-under=100" +ci-test.shell = "coverage run -m unittest discover" coverage.shell = "coverage run -m unittest discover && coverage report -m" format = "ruff format youtube_transcript_api" ci-format = "ruff format youtube_transcript_api --check" lint = "ruff check youtube_transcript_api" [tool.poetry.dependencies] -python = ">=3.8,<3.13" +python = ">=3.8,<=3.14" requests = "*" [tool.poetry.group.test] @@ -57,7 +57,6 @@ pytest = "^8.3.3" coverage = "^7.6.1" mock = "^5.1.0" httpretty = "^1.1.4" -coveralls = "^4.0.1" [tool.poetry.group.dev] optional = true From eef0b92eca0a94822d6a1661e9a8de07ca4f4091 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 12:08:24 +0100 Subject: [PATCH 10/31] fixed ci.yml formatting --- .github/workflows/ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afd4ac5..0bb3dc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,14 +50,14 @@ jobs: flag-name: run-python-${{ matrix.python-version }} parallel: true -finish: - needs: test - runs-on: ubuntu-latest - - steps: - - name: Finalize coverage report - uses: coverallsapp/github-action@v2 - with: - parallel-finished: true - carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13,run-python-3.14" + finish: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Finalize coverage report + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13,run-python-3.14" From 07eae03e94cbebd3a8bd4d22e8d7042ff583686d Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 12:09:59 +0100 Subject: [PATCH 11/31] removed python 3.14 build --- .github/workflows/ci.yml | 4 ++-- poetry.lock | 4 ++-- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bb3dc3..2f8cf2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 @@ -59,5 +59,5 @@ jobs: uses: coverallsapp/github-action@v2 with: parallel-finished: true - carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13,run-python-3.14" + carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13" diff --git a/poetry.lock b/poetry.lock index d0d5582..5906f03 100644 --- a/poetry.lock +++ b/poetry.lock @@ -411,5 +411,5 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" -python-versions = ">=3.8,<=3.14" -content-hash = "1fc5fdbba555a4380ff80fda6988a95ecbaa342ca863cf19f8fc8e3df0430ff7" +python-versions = ">=3.8,<3.14" +content-hash = "370c5c5f94f6000e0fdb76190a3aabd5acadf804802ca70dba41787d306799b4" diff --git a/pyproject.toml b/pyproject.toml index 03860b3..f0f43c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ ci-format = "ruff format youtube_transcript_api --check" lint = "ruff check youtube_transcript_api" [tool.poetry.dependencies] -python = ">=3.8,<=3.14" +python = ">=3.8,<3.14" requests = "*" [tool.poetry.group.test] From 3ecdcff08211c85155fffadde869b238f00dc433 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 12:20:32 +0100 Subject: [PATCH 12/31] coveralls command now specifies coverage format --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f8cf2f..944b8c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,8 @@ jobs: - name: Report intermediate coverage report uses: coverallsapp/github-action@v2 with: + file: .coverage + format: python flag-name: run-python-${{ matrix.python-version }} parallel: true From ed4068479cc5415f77363aa908b9c8c514957f01 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 12:24:52 +0100 Subject: [PATCH 13/31] coverage now is reported in cobertura format --- .github/workflows/ci.yml | 4 ++-- .gitignore | 1 + pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 944b8c9..2e17479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,8 +47,8 @@ jobs: - name: Report intermediate coverage report uses: coverallsapp/github-action@v2 with: - file: .coverage - format: python + file: coverage.xml + format: cobertura flag-name: run-python-${{ matrix.python-version }} parallel: true diff --git a/.gitignore b/.gitignore index 43b998b..492f752 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ build *.egg-info upload_new_version.sh .coverage +coverage.xml .DS_STORE \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f0f43c8..ba5397b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" [tool.poe.tasks] test = "pytest youtube_transcript_api" -ci-test.shell = "coverage run -m unittest discover" +ci-test.shell = "coverage run -m unittest discover && coverage xml" coverage.shell = "coverage run -m unittest discover && coverage report -m" format = "ruff format youtube_transcript_api" ci-format = "ruff format youtube_transcript_api --check" From 0967845a3d49e808858a0d1360e771f3b1e1e378 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 12:30:22 +0100 Subject: [PATCH 14/31] testing if coverall will fail build --- .github/workflows/ci.yml | 2 +- README.md | 2 +- youtube_transcript_api/test/test_api.py | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e17479..dffdfba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: flag-name: run-python-${{ matrix.python-version }} parallel: true - finish: + report-coverage: needs: test runs-on: ubuntu-latest diff --git a/README.md b/README.md index 980c565..e2cba01 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,7 @@ This code uses an undocumented part of the YouTube API, which is called by the Y To setup the project locally run (requires [poetry](https://python-poetry.org/docs/) to be installed): ```shell -poetry install +poetry install --with test,dev ``` There's [poe](https://github.com/nat-n/poethepoet?tab=readme-ov-file#quick-start) tasks to run tests, coverage, the linter and formatter (you'll need to pass all of those for the build to pass): diff --git a/youtube_transcript_api/test/test_api.py b/youtube_transcript_api/test/test_api.py index 3d2e48c..f7f909a 100644 --- a/youtube_transcript_api/test/test_api.py +++ b/youtube_transcript_api/test/test_api.py @@ -406,8 +406,8 @@ def test_load_cookies__bad_file_path(self): with self.assertRaises(CookiePathInvalid): YouTubeTranscriptApi._load_cookies(bad_cookies, "GJLlxj_dtq8") - def test_load_cookies__no_valid_cookies(self): - dirname, filename = os.path.split(os.path.abspath(__file__)) - expired_cookies = dirname + "/expired_example_cookies.txt" - with self.assertRaises(CookiesInvalid): - YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") + # def test_load_cookies__no_valid_cookies(self): + # dirname, filename = os.path.split(os.path.abspath(__file__)) + # expired_cookies = dirname + "/expired_example_cookies.txt" + # with self.assertRaises(CookiesInvalid): + # YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") From 339fc4bcfe4579c1a3a986787092c39c265e17eb Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 14:53:58 +0100 Subject: [PATCH 15/31] commented test back in --- youtube_transcript_api/test/test_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/youtube_transcript_api/test/test_api.py b/youtube_transcript_api/test/test_api.py index f7f909a..3d2e48c 100644 --- a/youtube_transcript_api/test/test_api.py +++ b/youtube_transcript_api/test/test_api.py @@ -406,8 +406,8 @@ def test_load_cookies__bad_file_path(self): with self.assertRaises(CookiePathInvalid): YouTubeTranscriptApi._load_cookies(bad_cookies, "GJLlxj_dtq8") - # def test_load_cookies__no_valid_cookies(self): - # dirname, filename = os.path.split(os.path.abspath(__file__)) - # expired_cookies = dirname + "/expired_example_cookies.txt" - # with self.assertRaises(CookiesInvalid): - # YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") + def test_load_cookies__no_valid_cookies(self): + dirname, filename = os.path.split(os.path.abspath(__file__)) + expired_cookies = dirname + "/expired_example_cookies.txt" + with self.assertRaises(CookiesInvalid): + YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") From c64cad639317ec57022bd5dc3a3da75855ff5158 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:03:05 +0100 Subject: [PATCH 16/31] added publish step to CI --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dffdfba..70a8871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,3 +63,24 @@ jobs: parallel-finished: true carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13" + publish: +# TODO +# if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + needs: report-coverage + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: 3.9 + - name: Install dependencies + run: | + pip install poetry + poetry install + - name: Build + run: poetry build + - name: Publish + # TODO update repo and username/pw + run: poetry publish -r testpypi -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} From f92e3d6595c62826d6fbfb4d6133946500452ea1 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:07:05 +0100 Subject: [PATCH 17/31] temporarly added testpypi url --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70a8871..2cad41a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,4 +83,6 @@ jobs: run: poetry build - name: Publish # TODO update repo and username/pw - run: poetry publish -r testpypi -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} + run: | + poetry config repositories.testpypi https://test.pypi.org/legacy/ + poetry publish -r testpypi -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} From 2a1525d583d008f04505ff026e00595de236446a Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:15:41 +0100 Subject: [PATCH 18/31] changed pypi publish to use token auth --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cad41a..ed124ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,4 +85,4 @@ jobs: # TODO update repo and username/pw run: | poetry config repositories.testpypi https://test.pypi.org/legacy/ - poetry publish -r testpypi -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} + poetry publish -r testpypi -u __token__ -p ${{ secrets.PYPI_TOKEN_TEST }} From 48cce90d5a32658a2eda6d4933e57fa1e0a1c8b8 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:21:13 +0100 Subject: [PATCH 19/31] temporary change to test publish in CI --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ba5397b..4c7e04f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,8 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "youtube-transcript-api" -version = "0.6.2" +# TODO change back to .2 +version = "0.6.3" description = "This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. It also works for automatically generated subtitles, supports translating subtitles and it does not require a headless browser, like other selenium based solutions do!" readme = "README.md" license = "MIT" From ee996f77f146641c6f1e12e388e301d153db1172 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:26:11 +0100 Subject: [PATCH 20/31] updated test PyPi URL --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed124ea..6260849 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,5 +84,5 @@ jobs: - name: Publish # TODO update repo and username/pw run: | - poetry config repositories.testpypi https://test.pypi.org/legacy/ + poetry config repositories.testpypi https://test.pypi.org/simple/ poetry publish -r testpypi -u __token__ -p ${{ secrets.PYPI_TOKEN_TEST }} diff --git a/pyproject.toml b/pyproject.toml index 4c7e04f..0215643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" [tool.poe.tasks] test = "pytest youtube_transcript_api" -ci-test.shell = "coverage run -m unittest discover && coverage xml" +ci-test.shell = "coverage run -m unittest discover && coverage xml --fail-under=100" coverage.shell = "coverage run -m unittest discover && coverage report -m" format = "ruff format youtube_transcript_api" ci-format = "ruff format youtube_transcript_api --check" From 7881a1d8979aca658e0e0f60aed0ddf14bf231ce Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:37:47 +0100 Subject: [PATCH 21/31] publish is now automatically executed for new tags --- .github/workflows/ci.yml | 8 ++------ pyproject.toml | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6260849..e80d980 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,8 +64,7 @@ jobs: carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13" publish: -# TODO -# if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') needs: report-coverage runs-on: ubuntu-latest @@ -82,7 +81,4 @@ jobs: - name: Build run: poetry build - name: Publish - # TODO update repo and username/pw - run: | - poetry config repositories.testpypi https://test.pypi.org/simple/ - poetry publish -r testpypi -u __token__ -p ${{ secrets.PYPI_TOKEN_TEST }} + run: poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }} diff --git a/pyproject.toml b/pyproject.toml index 0215643..ead1047 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "youtube-transcript-api" -# TODO change back to .2 -version = "0.6.3" +version = "0.6.2" description = "This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. It also works for automatically generated subtitles, supports translating subtitles and it does not require a headless browser, like other selenium based solutions do!" readme = "README.md" license = "MIT" From 47eae6d8e0fa58d04867d9bd0b80a1c69be4bc4c Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:38:14 +0100 Subject: [PATCH 22/31] temporary change to test coverage in CI --- youtube_transcript_api/test/test_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/youtube_transcript_api/test/test_api.py b/youtube_transcript_api/test/test_api.py index 3d2e48c..f7f909a 100644 --- a/youtube_transcript_api/test/test_api.py +++ b/youtube_transcript_api/test/test_api.py @@ -406,8 +406,8 @@ def test_load_cookies__bad_file_path(self): with self.assertRaises(CookiePathInvalid): YouTubeTranscriptApi._load_cookies(bad_cookies, "GJLlxj_dtq8") - def test_load_cookies__no_valid_cookies(self): - dirname, filename = os.path.split(os.path.abspath(__file__)) - expired_cookies = dirname + "/expired_example_cookies.txt" - with self.assertRaises(CookiesInvalid): - YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") + # def test_load_cookies__no_valid_cookies(self): + # dirname, filename = os.path.split(os.path.abspath(__file__)) + # expired_cookies = dirname + "/expired_example_cookies.txt" + # with self.assertRaises(CookiesInvalid): + # YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") From 93fe4323f536659a4e567e5f2518ca9459d2d0e2 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:41:12 +0100 Subject: [PATCH 23/31] temporary change to test coverage in CI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ead1047..ba5397b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" [tool.poe.tasks] test = "pytest youtube_transcript_api" -ci-test.shell = "coverage run -m unittest discover && coverage xml --fail-under=100" +ci-test.shell = "coverage run -m unittest discover && coverage xml" coverage.shell = "coverage run -m unittest discover && coverage report -m" format = "ruff format youtube_transcript_api" ci-format = "ruff format youtube_transcript_api --check" From d631ceb03c14ced2237b82b25c75d2776d14c5f5 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 15:57:34 +0100 Subject: [PATCH 24/31] coverage report stage will now fail immediatelly --- .github/workflows/ci.yml | 13 ++++++++++++- pyproject.toml | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e80d980..bbfe110 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,10 +62,21 @@ jobs: with: parallel-finished: true carryforward: "run-python-3.8,run-python-3.9,run-python-3.10,run-python-3.11,run-python-3.12,run-python-3.13" + - uses: actions/checkout@v4 + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: 3.9 + - name: Install dependencies + run: | + pip install poetry poethepoet + poetry install --only test + - name: Check coverage + run: poe coverage-report publish: if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - needs: report-coverage + needs: [report-coverage, static-checks] runs-on: ubuntu-latest steps: diff --git a/pyproject.toml b/pyproject.toml index ba5397b..f410135 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,8 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" [tool.poe.tasks] test = "pytest youtube_transcript_api" ci-test.shell = "coverage run -m unittest discover && coverage xml" -coverage.shell = "coverage run -m unittest discover && coverage report -m" +coverage.shell = "coverage run -m unittest discover && coverage report -m --fail-under=100" +coverage-report = "coverage report -m --fail-under=100" format = "ruff format youtube_transcript_api" ci-format = "ruff format youtube_transcript_api --check" lint = "ruff check youtube_transcript_api" From c738da22db4fc9a35c263d42ae61546339241770 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 16:01:35 +0100 Subject: [PATCH 25/31] fixed missing coverage data --- .github/workflows/ci.yml | 4 ++-- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbfe110..4566f24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: flag-name: run-python-${{ matrix.python-version }} parallel: true - report-coverage: + coverage: needs: test runs-on: ubuntu-latest @@ -72,7 +72,7 @@ jobs: pip install poetry poethepoet poetry install --only test - name: Check coverage - run: poe coverage-report + run: poe coverage publish: if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') diff --git a/pyproject.toml b/pyproject.toml index f410135..2cb4a0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ youtube_transcript_api = "youtube_transcript_api.__main__:main" test = "pytest youtube_transcript_api" ci-test.shell = "coverage run -m unittest discover && coverage xml" coverage.shell = "coverage run -m unittest discover && coverage report -m --fail-under=100" -coverage-report = "coverage report -m --fail-under=100" format = "ruff format youtube_transcript_api" ci-format = "ruff format youtube_transcript_api --check" lint = "ruff check youtube_transcript_api" From 9bb8794cd6a1ff24d77217ddb6ac87c22b5bab55 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 16:02:29 +0100 Subject: [PATCH 26/31] fixed typo in ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4566f24..32d8ba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: publish: if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - needs: [report-coverage, static-checks] + needs: [coverage, static-checks] runs-on: ubuntu-latest steps: From 402b4cb95d8f80bf2d4190839215bffd22f61e3d Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 16:04:17 +0100 Subject: [PATCH 27/31] fixed coverage stage --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32d8ba2..9904ab5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: - name: Install dependencies run: | pip install poetry poethepoet - poetry install --only test + poetry install --with test - name: Check coverage run: poe coverage From 006edf9c91d13e52fcb8d01308adc2dc0f651e12 Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 16:06:00 +0100 Subject: [PATCH 28/31] commented test back in --- youtube_transcript_api/test/test_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/youtube_transcript_api/test/test_api.py b/youtube_transcript_api/test/test_api.py index f7f909a..3d2e48c 100644 --- a/youtube_transcript_api/test/test_api.py +++ b/youtube_transcript_api/test/test_api.py @@ -406,8 +406,8 @@ def test_load_cookies__bad_file_path(self): with self.assertRaises(CookiePathInvalid): YouTubeTranscriptApi._load_cookies(bad_cookies, "GJLlxj_dtq8") - # def test_load_cookies__no_valid_cookies(self): - # dirname, filename = os.path.split(os.path.abspath(__file__)) - # expired_cookies = dirname + "/expired_example_cookies.txt" - # with self.assertRaises(CookiesInvalid): - # YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") + def test_load_cookies__no_valid_cookies(self): + dirname, filename = os.path.split(os.path.abspath(__file__)) + expired_cookies = dirname + "/expired_example_cookies.txt" + with self.assertRaises(CookiesInvalid): + YouTubeTranscriptApi._load_cookies(expired_cookies, "GJLlxj_dtq8") From 00642578d47b2a3b9782517fab983e735425e56a Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 16:14:58 +0100 Subject: [PATCH 29/31] added GH Action build status badge to README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2cba01..a5eeecf 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ Donate - - Build Status + + Build Status Coverage Status From 32ea9c1b5bc268021770cf7e9811f8ab1382a44d Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 16:16:53 +0100 Subject: [PATCH 30/31] deleted travis.yml --- .travis.yml | 15 -------------- setup.py | 59 ----------------------------------------------------- 2 files changed, 74 deletions(-) delete mode 100644 .travis.yml delete mode 100644 setup.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fef3592..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: python -python: - - "3.5" - - "3.6" - - "3.7.11" - - "3.8" -install: - - pip install --upgrade pip - - pip install --upgrade setuptools - - pip install -r requirements.txt - - pip install urllib3==1.26.6 -script: - - coverage run -m unittest discover -after_success: - - coveralls diff --git a/setup.py b/setup.py deleted file mode 100644 index b2000a6..0000000 --- a/setup.py +++ /dev/null @@ -1,59 +0,0 @@ -import os - -import unittest - -import setuptools - - -def _get_file_content(file_name): - with open(file_name, 'r') as file_handler: - return file_handler.read() - -def get_long_description(): - return _get_file_content('README.md') - - -def get_test_suite(): - test_loader = unittest.TestLoader() - test_suite = test_loader.discover( - 'test', pattern='test_*.py', - top_level_dir='{dirname}/youtube_transcript_api'.format(dirname=os.path.dirname(__file__)) - ) - return test_suite - - -setuptools.setup( - name="youtube_transcript_api", - version="0.6.2", - author="Jonas Depoix", - author_email="jonas.depoix@web.de", - description="This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. It also works for automatically generated subtitles, supports translating subtitles and it does not require a headless browser, like other selenium based solutions do!", - long_description=get_long_description(), - long_description_content_type="text/markdown", - keywords="youtube-api subtitles youtube transcripts transcript subtitle youtube-subtitles youtube-transcripts cli", - url="https://github.com/jdepoix/youtube-transcript-api", - packages=setuptools.find_packages(), - classifiers=( - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ), - install_requires=[ - 'requests', - ], - tests_require=[ - 'mock', - 'httpretty', - 'coverage', - 'coveralls', - ], - test_suite='setup.get_test_suite', - entry_points={ - 'console_scripts': [ - 'youtube_transcript_api = youtube_transcript_api.__main__:main', - ], - }, -) From ec3fbe9be722d2a74b4363cfd717259978cd8edb Mon Sep 17 00:00:00 2001 From: Jonas Depoix Date: Mon, 11 Nov 2024 16:26:06 +0100 Subject: [PATCH 31/31] added precommit poe task --- README.md | 5 +++++ pyproject.toml | 1 + 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index a5eeecf..87d3723 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,11 @@ poe format poe lint ``` +If you just want to make sure that your code passes all the necessary checks to get a green build, you can simply run: +```shell +poe precommit +``` + ## Donations If this project makes you happy by reducing your development time, you can make me happy by treating me to a cup of coffee, or become a [Sponsor of this project](https://github.com/sponsors/jdepoix) :) diff --git a/pyproject.toml b/pyproject.toml index 2cb4a0c..ad6de10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ coverage.shell = "coverage run -m unittest discover && coverage report -m --fail format = "ruff format youtube_transcript_api" ci-format = "ruff format youtube_transcript_api --check" lint = "ruff check youtube_transcript_api" +precommit.shell = "poe format && poe lint && poe coverage" [tool.poetry.dependencies] python = ">=3.8,<3.14"