Skip to content

Commit

Permalink
drop 3.8, add pytest-retry (#592)
Browse files Browse the repository at this point in the history
* drop 3.8, add pytest-retry

* fix mypy

* fix missing import
  • Loading branch information
sigma67 authored Jul 13, 2024
1 parent 08d9f30 commit 828b1b9
Show file tree
Hide file tree
Showing 26 changed files with 321 additions and 362 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ jobs:
curl -o tests/test.mp3 https://www.kozco.com/tech/piano2-CoolEdit.mp3
cat <<< "$HEADERS_AUTH" > tests/browser.json
cat <<< "$TEST_CFG" > tests/test.cfg
(echo "===== tests attempt: 1 ====" && pdm run pytest) || \
(echo "===== tests attempt: 2 ====" && pdm run pytest)
pdm run pytest
pdm run coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.8"
python-version: "3.9"
- run: pip install mypy==1.10.0
- run: mypy --install-types --non-interactive
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.0
rev: v0.5.1
hooks:
# Run the linter.
- id: ruff
Expand Down
410 changes: 180 additions & 230 deletions pdm.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "ytmusicapi"
description = "Unofficial API for YouTube Music"
requires-python = ">=3.8"
requires-python = ">=3.9"
authors=[{name = "sigma67", email= "ytmusicapi@gmail.com"}]
license={file="LICENSE"}
classifiers = [
Expand Down Expand Up @@ -43,7 +43,7 @@ include-package-data=false
[tool.pytest.ini_options]
python_functions = "test_*"
testpaths = ["tests"]
addopts = "--verbose --cov"
addopts = "--verbose --cov --retries 2 --retry-delay 5"

[tool.coverage.run]
source = ["ytmusicapi"]
Expand Down Expand Up @@ -78,4 +78,5 @@ dev = [
"pytest>=7.4.4",
"pytest-cov>=4.1.0",
"types-requests>=2.31.0.20240218",
"pytest-retry>=1.6.3",
]
12 changes: 7 additions & 5 deletions tests/auth/test_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import tempfile
import time
from pathlib import Path
from typing import Any, Dict
from typing import Any
from unittest import mock

import pytest
Expand All @@ -17,7 +17,7 @@


@pytest.fixture(name="blank_code")
def fixture_blank_code() -> Dict[str, Any]:
def fixture_blank_code() -> dict[str, Any]:
return {
"device_code": "",
"user_code": "",
Expand Down Expand Up @@ -46,9 +46,11 @@ def test_setup_oauth(self, session_mock, json_mock, blank_code, config):
json_mock.side_effect = [blank_code, token_code]
oauth_file = tempfile.NamedTemporaryFile(delete=False)
oauth_filepath = oauth_file.name
with mock.patch("builtins.input", return_value="y"), mock.patch(
"sys.argv", ["ytmusicapi", "oauth", "--file", oauth_filepath]
), mock.patch("webbrowser.open"):
with (
mock.patch("builtins.input", return_value="y"),
mock.patch("sys.argv", ["ytmusicapi", "oauth", "--file", oauth_filepath]),
mock.patch("webbrowser.open"),
):
main()
assert Path(oauth_filepath).exists()

Expand Down
9 changes: 5 additions & 4 deletions tests/mixins/test_playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from ytmusicapi import YTMusic
from ytmusicapi.constants import SUPPORTED_LANGUAGES
from ytmusicapi.enums import ResponseStatus


class TestPlaylists:
Expand Down Expand Up @@ -85,7 +86,7 @@ def test_edit_playlist(self, config, yt_brand):
playlist["tracks"][0]["setVideoId"],
),
)
assert response1 == "STATUS_SUCCEEDED", "Playlist edit 1 failed"
assert response1 == ResponseStatus.SUCCEEDED, "Playlist edit 1 failed"
response2 = yt_brand.edit_playlist(
playlist["id"],
title=playlist["title"],
Expand All @@ -96,7 +97,7 @@ def test_edit_playlist(self, config, yt_brand):
playlist["tracks"][1]["setVideoId"],
),
)
assert response2 == "STATUS_SUCCEEDED", "Playlist edit 2 failed"
assert response2 == ResponseStatus.SUCCEEDED, "Playlist edit 2 failed"
response3 = yt_brand.edit_playlist(
playlist["id"],
title=playlist["title"],
Expand All @@ -120,13 +121,13 @@ def test_end2end(self, yt_brand, sample_video):
source_playlist="OLAK5uy_nvjTE32aFYdFN7HCyMv3cGqD3wqBb4Jow",
duplicates=True,
)
assert response["status"] == "STATUS_SUCCEEDED", "Adding playlist item failed"
assert response["status"] == ResponseStatus.SUCCEEDED, "Adding playlist item failed"
assert len(response["playlistEditResults"]) > 0, "Adding playlist item failed"
time.sleep(3)
yt_brand.edit_playlist(playlist_id, addToTop=False)
time.sleep(3)
playlist = yt_brand.get_playlist(playlist_id, related=True)
assert len(playlist["tracks"]) == 46, "Getting playlist items failed"
response = yt_brand.remove_playlist_items(playlist_id, playlist["tracks"])
assert response == "STATUS_SUCCEEDED", "Playlist item removal failed"
assert response == ResponseStatus.SUCCEEDED, "Playlist item removal failed"
yt_brand.delete_playlist(playlist_id)
12 changes: 7 additions & 5 deletions tests/mixins/test_uploads.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

from tests.conftest import get_resource
from ytmusicapi.enums import ResponseStatus
from ytmusicapi.ytmusic import YTMusic


Expand Down Expand Up @@ -38,8 +39,9 @@ def test_get_library_upload_artists(self, config, yt_oauth, yt_empty):
def test_upload_song_exceptions(self, config, yt_auth, yt_oauth):
with pytest.raises(Exception, match="The provided file does not exist."):
yt_auth.upload_song("song.wav")
with tempfile.NamedTemporaryFile(suffix="wav") as temp, pytest.raises(
Exception, match="The provided file type is not supported"
with (
tempfile.NamedTemporaryFile(suffix="wav") as temp,
pytest.raises(Exception, match="The provided file type is not supported"),
):
yt_auth.upload_song(temp.name)
with pytest.raises(Exception, match="Please provide browser authentication"):
Expand All @@ -59,14 +61,14 @@ def test_upload_song_and_verify(self, config, yt_auth: YTMusic):
for song in songs:
if song.get("title") in config["uploads"]["file"]:
delete_response = yt_auth.delete_upload_entity(song["entityId"])
assert delete_response == "STATUS_SUCCEEDED"
assert delete_response == ResponseStatus.SUCCEEDED
# Need to wait for song to be fully deleted
time.sleep(10)
# Now re-upload
upload_response = yt_auth.upload_song(get_resource(config["uploads"]["file"]))

assert (
upload_response == "STATUS_SUCCEEDED" or upload_response.status_code == 200
upload_response == ResponseStatus.SUCCEEDED or upload_response.status_code == 200
), f"Song failed to upload {upload_response}"

# Wait for upload to finish processing and verify it can be retrieved
Expand All @@ -86,7 +88,7 @@ def test_upload_song_and_verify(self, config, yt_auth: YTMusic):
def test_delete_upload_entity(self, yt_oauth):
results = yt_oauth.get_library_upload_songs()
response = yt_oauth.delete_upload_entity(results[0]["entityId"])
assert response == "STATUS_SUCCEEDED"
assert response == ResponseStatus.SUCCEEDED

def test_get_library_upload_album(self, config, yt_oauth):
album = yt_oauth.get_library_upload_album(config["uploads"]["private_album_id"])
Expand Down
5 changes: 3 additions & 2 deletions ytmusicapi/auth/oauth/credentials.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC, abstractmethod
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Dict, Mapping, Optional
from typing import Optional

import requests

Expand Down Expand Up @@ -48,7 +49,7 @@ def __init__(
client_id: Optional[str] = None,
client_secret: Optional[str] = None,
session: Optional[requests.Session] = None,
proxies: Optional[Dict] = None,
proxies: Optional[dict] = None,
):
"""
:param client_id: Optional. Set the GoogleAPI client_id used for auth flows.
Expand Down
3 changes: 1 addition & 2 deletions ytmusicapi/auth/types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""enum representing types of authentication supported by this library"""

from enum import Enum, auto
from typing import List


class AuthType(int, Enum):
Expand All @@ -21,5 +20,5 @@ class AuthType(int, Enum):
OAUTH_CUSTOM_FULL = auto()

@classmethod
def oauth_types(cls) -> List["AuthType"]:
def oauth_types(cls) -> list["AuthType"]:
return [cls.OAUTH_DEFAULT, cls.OAUTH_CUSTOM_CLIENT, cls.OAUTH_CUSTOM_FULL]
5 changes: 5 additions & 0 deletions ytmusicapi/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from enum import Enum


class ResponseStatus(str, Enum):
SUCCEEDED = "STATUS_SUCCEEDED"
10 changes: 5 additions & 5 deletions ytmusicapi/mixins/_protocol.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""protocol that defines the functions available to mixins"""

from typing import Dict, Optional, Protocol
from typing import Optional, Protocol

from requests import Response

Expand All @@ -15,17 +15,17 @@ class MixinProtocol(Protocol):

parser: Parser

proxies: Optional[Dict[str, str]]
proxies: Optional[dict[str, str]]

def _check_auth(self) -> None:
"""checks if self has authentication"""

def _send_request(self, endpoint: str, body: Dict, additionalParams: str = "") -> Dict:
def _send_request(self, endpoint: str, body: dict, additionalParams: str = "") -> dict:
"""for sending post requests to YouTube Music"""

def _send_get_request(self, url: str, params: Optional[Dict] = None) -> Response:
def _send_get_request(self, url: str, params: Optional[dict] = None) -> Response:
"""for sending get requests to YouTube Music"""

@property
def headers(self) -> Dict[str, str]:
def headers(self) -> dict[str, str]:
"""property for getting request headers"""
24 changes: 12 additions & 12 deletions ytmusicapi/mixins/browsing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
import warnings
from typing import Any, Dict, List, Optional
from typing import Any, Optional

from ytmusicapi.continuations import (
get_continuations,
Expand All @@ -18,7 +18,7 @@


class BrowsingMixin(MixinProtocol):
def get_home(self, limit=3) -> List[Dict]:
def get_home(self, limit=3) -> list[dict]:
"""
Get the home page.
The home page is structured as titled rows, returning 3 rows of music suggestions at a time.
Expand Down Expand Up @@ -124,7 +124,7 @@ def get_home(self, limit=3) -> List[Dict]:

return home

def get_artist(self, channelId: str) -> Dict:
def get_artist(self, channelId: str) -> dict:
"""
Get information about an artist and their top releases (songs,
albums, singles, videos, and related artists). The top lists
Expand Down Expand Up @@ -237,7 +237,7 @@ def get_artist(self, channelId: str) -> Dict:
response = self._send_request(endpoint, body)
results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST)

artist: Dict[str, Any] = {"description": None, "views": None}
artist: dict[str, Any] = {"description": None, "views": None}
header = response["header"]["musicImmersiveHeaderRenderer"]
artist["name"] = nav(header, TITLE_TEXT)
descriptionShelf = find_object_by_key(results, DESCRIPTION_SHELF[0], is_key=True)
Expand Down Expand Up @@ -271,7 +271,7 @@ def get_artist(self, channelId: str) -> Dict:

def get_artist_albums(
self, channelId: str, params: str, limit: Optional[int] = 100, order: Optional[str] = None
) -> List[Dict]:
) -> list[dict]:
"""
Get the full list of an artist's albums, singles or shows
Expand Down Expand Up @@ -350,7 +350,7 @@ def get_artist_albums(

return albums

def get_user(self, channelId: str) -> Dict:
def get_user(self, channelId: str) -> dict:
"""
Retrieve a user's page. A user may own videos or playlists.
Expand Down Expand Up @@ -406,7 +406,7 @@ def get_user(self, channelId: str) -> Dict:
user.update(self.parser.parse_channel_contents(results))
return user

def get_user_playlists(self, channelId: str, params: str) -> List[Dict]:
def get_user_playlists(self, channelId: str, params: str) -> list[dict]:
"""
Retrieve a list of playlists for a given user.
Call this function again with the returned ``params`` to get the full list.
Expand Down Expand Up @@ -448,7 +448,7 @@ def get_album_browse_id(self, audioPlaylistId: str) -> Optional[str]:
browse_id = matches.group().strip('"')
return browse_id

def get_album(self, browseId: str) -> Dict:
def get_album(self, browseId: str) -> dict:
"""
Get information and tracks of an album
Expand Down Expand Up @@ -532,7 +532,7 @@ def get_album(self, browseId: str) -> Dict:

return album

def get_song(self, videoId: str, signatureTimestamp: Optional[int] = None) -> Dict:
def get_song(self, videoId: str, signatureTimestamp: Optional[int] = None) -> dict:
"""
Returns metadata and streaming information about a song or video.
Expand Down Expand Up @@ -799,7 +799,7 @@ def get_song_related(self, browseId: str):
sections = nav(response, ["contents", *SECTION_LIST])
return parse_mixed_content(sections)

def get_lyrics(self, browseId: str) -> Dict:
def get_lyrics(self, browseId: str) -> dict:
"""
Returns lyrics of a song or video.
Expand Down Expand Up @@ -859,7 +859,7 @@ def get_signatureTimestamp(self, url: Optional[str] = None) -> int:

return int(match.group(1))

def get_tasteprofile(self) -> Dict:
def get_tasteprofile(self) -> dict:
"""
Fetches suggested artists from taste profile (music.youtube.com/tasteprofile).
Tasteprofile allows users to pick artists to update their recommendations.
Expand Down Expand Up @@ -891,7 +891,7 @@ def get_tasteprofile(self) -> Dict:
}
return taste_profiles

def set_tasteprofile(self, artists: List[str], taste_profile: Optional[Dict] = None) -> None:
def set_tasteprofile(self, artists: list[str], taste_profile: Optional[dict] = None) -> None:
"""
Favorites artists to see more recommendations from the artist.
Use :py:func:`get_tasteprofile` to see which artists are available to be recommended
Expand Down
Loading

0 comments on commit 828b1b9

Please sign in to comment.