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/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9904ab5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,95 @@
+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@v5
+ with:
+ python-version: 3.9
+ - name: Install dependencies
+ run: |
+ pip install poetry poethepoet
+ 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", "3.13"]
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ pip install poetry poethepoet
+ poetry install --with test
+ - name: Run tests
+ run: |
+ poe ci-test
+ - name: Report intermediate coverage report
+ uses: coverallsapp/github-action@v2
+ with:
+ file: coverage.xml
+ format: cobertura
+ flag-name: run-python-${{ matrix.python-version }}
+ parallel: true
+
+ coverage:
+ 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"
+ - 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 --with test
+ - name: Check coverage
+ run: poe coverage
+
+ publish:
+ if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+ needs: [coverage, static-checks]
+ 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
+ run: poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 542d724..492f752 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,6 @@ dist
build
*.egg-info
upload_new_version.sh
-.coverage
\ No newline at end of file
+.coverage
+coverage.xml
+.DS_STORE
\ No newline at end of file
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/README.md b/README.md
index d323d5e..87d3723 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@
-
-
+
+
@@ -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,29 @@ 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 --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):
+```shell
+poe test
+poe coverage
+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
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..5906f03
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,415 @@
+# 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.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.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]]
+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.extras]
+toml = ["tomli"]
+
+[[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.2"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
+]
+
+[[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 = "ruff"
+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.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.2"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
+ {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
+]
+
+[[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.14"
+content-hash = "370c5c5f94f6000e0fdb76190a3aabd5acadf804802ca70dba41787d306799b4"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ad6de10
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,94 @@
+[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"
+ci-test.shell = "coverage run -m unittest discover && coverage xml"
+coverage.shell = "coverage run -m unittest discover && 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"
+precommit.shell = "poe format && poe lint && poe coverage"
+
+[tool.poetry.dependencies]
+python = ">=3.8,<3.14"
+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"
+
+[tool.poetry.group.dev]
+optional = true
+
+[tool.poetry.group.dev.dependencies]
+ruff = "^0.6.8"
+
+[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
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',
- ],
- },
-)
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/__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..f93f717 100644
--- a/youtube_transcript_api/_transcripts.py
+++ b/youtube_transcript_api/_transcripts.py
@@ -2,8 +2,9 @@
# 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')
+ sys.setdefaultencoding("utf-8")
import json
@@ -52,7 +53,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 +63,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 +77,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 +91,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 +103,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 +143,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 +176,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 +195,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 +226,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 +236,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 +306,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 +318,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 +329,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 +346,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 +358,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 +375,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..dd21b39 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")