From 3a5d85246868a1f39bbdbd268511df18fb461244 Mon Sep 17 00:00:00 2001 From: ludehon Date: Sun, 1 Sep 2019 19:22:18 +0200 Subject: [PATCH 01/44] Added vcr support for consistent tests --- .gitignore | 7 +++++++ tests/test_genius.py | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0619f86e..d8b4dec8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,10 @@ __pycache__ dist build .idea + +# VCR cache files +test_get_annotation +test_get_referents_web_page +test_get_song_annotations +test_search_genius_web +test_search_song \ No newline at end of file diff --git a/tests/test_genius.py b/tests/test_genius.py index a980d2e1..28e5ea9e 100644 --- a/tests/test_genius.py +++ b/tests/test_genius.py @@ -3,6 +3,9 @@ from lyricsgenius.api import Genius from lyricsgenius.song import Song from lyricsgenius.artist import Artist +import vcr +import sys +sys.path.append('..') # Import client access token from environment variable client_access_token = os.environ.get("GENIUS_CLIENT_ACCESS_TOKEN", None) @@ -18,12 +21,14 @@ def setUpClass(cls): cls.search_term = "Ezra Furman" cls.song_title_only = "99 Problems" + @vcr.use_cassette() def test_search_genius_web(self): # TODO: test more than just a 200 response msg = "Response was None." r = genius.search_genius_web(self.search_term) self.assertTrue(r is not None, msg) + @vcr.use_cassette() def test_search_song(self): artist = "Jay-Z" drake_song = "All Me" @@ -47,6 +52,7 @@ def test_search_song(self): response = genius.search_song(self.song_title_only, artist="Drake") self.assertFalse(response.title.lower() == self.song_title_only.lower()) + @vcr.use_cassette() def test_get_referents_web_page(self): msg = "Returned referent API path is different than expected." id_ = 10347 @@ -65,6 +71,7 @@ def test_get_referents_no_inputs(self): with self.assertRaises(AssertionError): genius.get_referents() + @vcr.use_cassette() def test_get_annotation(self): msg = "Returned annotation API path is different than expected." id_ = 10225840 @@ -73,10 +80,11 @@ def test_get_annotation(self): expected = '/annotations/10225840' self.assertEqual(real, expected, msg) + @vcr.use_cassette() def test_get_song_annotations(self): msg = "Incorrect song annotation response." id_ = 1 - r = sorted(genius.get_song_annotations(1)) + r = sorted(genius.get_song_annotations(id_)) real = r[0][0] expected = "And I’ma keep ya fresh" self.assertEqual(real, expected, msg) @@ -85,6 +93,7 @@ def test_get_song_annotations(self): class TestArtist(unittest.TestCase): @classmethod + @vcr.use_cassette() def setUpClass(cls): print("\n---------------------\nSetting up Artist tests...\n") cls.artist_name = "The Beatles" @@ -97,6 +106,7 @@ def test_artist(self): msg = "The returned object is not an instance of the Artist class." self.assertIsInstance(self.artist, Artist, msg) + @vcr.use_cassette() def test_correct_artist_name(self): msg = "Returned artist name does not match searched artist." name = "Queen" @@ -107,11 +117,13 @@ def test_name(self): msg = "The artist object name does not match the requested artist name." self.assertEqual(self.artist.name, self.artist_name, msg) + @vcr.use_cassette() def test_add_song_from_same_artist(self): msg = "The new song was not added to the artist object." self.artist.add_song(genius.search_song(self.new_song, self.artist_name)) self.assertEqual(self.artist.num_songs, self.max_songs+1, msg) + @vcr.use_cassette() def test_add_song_from_different_artist(self): msg = "A song from a different artist was incorrectly allowed to be added." self.artist.add_song(genius.search_song("These Days", "Jackson Browne")) @@ -180,6 +192,7 @@ def test_saving_txt_file(self): class TestSong(unittest.TestCase): @classmethod + @vcr.use_cassette() def setUpClass(cls): print("\n---------------------\nSetting up Song tests...\n") cls.artist_name = 'Andy Shauf' From 69dc4c3fe9db20d9835e7b9f18c5b4b245c9c22b Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 8 Nov 2020 12:52:41 +0330 Subject: [PATCH 02/44] added vcr to tests --- tests/test_album.py | 8 +++-- tests/test_api.py | 9 +++-- tests/test_artist.py | 18 ++++++++-- tests/test_base.py | 5 +-- tests/test_genius.py | 18 ++++++++++ tests/test_public_methods.py | 66 ++++++++++++++++++++++++++++++++++++ tests/test_song.py | 24 ++++--------- 7 files changed, 122 insertions(+), 26 deletions(-) diff --git a/tests/test_album.py b/tests/test_album.py index e387e239..f5d8e013 100644 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -1,16 +1,20 @@ import unittest import os +import vcr + try: - from .test_genius import genius + from .test_genius import genius, test_vcr except ModuleNotFoundError: - from test_genius import genius + from test_genius import genius, test_vcr from lyricsgenius.types import Album class TestAlbum(unittest.TestCase): @classmethod + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix(' album.yaml'), + serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Album tests...\n") cls.album_name = "The Party" diff --git a/tests/test_api.py b/tests/test_api.py index 41170c9e..bdd2e6c2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,9 @@ import unittest try: - from .test_genius import genius + from .test_genius import genius, test_vcr except ModuleNotFoundError: - from test_genius import genius + from test_genius import genius, test_vcr class TestAPI(unittest.TestCase): @@ -12,12 +12,14 @@ class TestAPI(unittest.TestCase): def setUpClass(cls): print("\n---------------------\nSetting up API tests...\n") + @test_vcr.use_cassette def test_account(self): msg = ("No user detail was returned. " "Are you sure you're using a user access token?") r = genius.account() self.assertTrue("user" in r, msg) + @test_vcr.use_cassette def test_annotation(self): msg = "Returned annotation API path is different than expected." id_ = 10225840 @@ -26,6 +28,7 @@ def test_annotation(self): expected = '/annotations/10225840' self.assertEqual(real, expected, msg) + @test_vcr.use_cassette def test_manage_annotation(self): example_text = 'The annotation' new_annotation = genius.create_annotation( @@ -64,6 +67,7 @@ def test_manage_annotation(self): r = genius.delete_annotation(new_annotation['id']) self.assertEqual(r, 204, msg) + @test_vcr.use_cassette def test_referents_web_page(self): msg = "Returned referent API path is different than expected." id_ = 10347 @@ -82,6 +86,7 @@ def test_referents_invalid_input(self): with self.assertRaises(AssertionError): genius.referents(song_id=1, web_page_id=1) + @test_vcr.use_cassette def test_web_page(self): msg = "Returned web page API path is different than expected." url = "https://docs.genius.com" diff --git a/tests/test_artist.py b/tests/test_artist.py index 82eafcba..e4dce9e7 100644 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -1,10 +1,12 @@ import unittest import os +import vcr + try: - from .test_genius import genius + from .test_genius import genius, test_vcr except ModuleNotFoundError: - from test_genius import genius + from test_genius import genius, test_vcr from lyricsgenius.types import Artist from lyricsgenius.utils import sanitize_filename @@ -12,6 +14,8 @@ class TestArtist(unittest.TestCase): @classmethod + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix(' artist.yaml'), + serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Artist tests...\n") cls.artist_name = "The Beatles" @@ -24,12 +28,15 @@ def test_artist(self): msg = "The returned object is not an instance of the Artist class." self.assertIsInstance(self.artist, Artist, msg) + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml') def test_correct_artist_name(self): msg = "Returned artist name does not match searched artist." name = "Queen" result = genius.search_artist(name, max_songs=1).name self.assertEqual(name, result, msg) + @test_vcr.use_cassette def test_zero_songs(self): msg = "Songs were downloaded even though 0 songs was requested." name = "Queen" @@ -40,21 +47,28 @@ def test_name(self): msg = "The artist object name does not match the requested artist name." self.assertEqual(self.artist.name, self.artist_name, msg) + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml') def test_add_song_from_same_artist(self): msg = "The new song was not added to the artist object." self.artist.add_song(genius.search_song(self.new_song, self.artist_name)) self.assertEqual(self.artist.num_songs, self.max_songs + 1, msg) + @test_vcr.use_cassette def test_song(self): msg = "Song was not in artist's songs." song = self.artist.song(self.new_song) self.assertIsNotNone(song, msg) + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml') def test_add_song_from_different_artist(self): msg = "A song from a different artist was incorrectly allowed to be added." self.artist.add_song(genius.search_song("These Days", "Jackson Browne")) self.assertEqual(self.artist.num_songs, self.max_songs, msg) + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml') def test_artist_with_includes_features(self): # The artist did not get songs returned that they were featured in. name = "Swae Lee" diff --git a/tests/test_base.py b/tests/test_base.py index 9ae7ff51..31dabed8 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -3,9 +3,9 @@ from requests.exceptions import HTTPError try: - from .test_genius import genius + from .test_genius import genius, test_vcr except ModuleNotFoundError: - from test_genius import genius + from test_genius import genius, test_vcr class TestAPIBase(unittest.TestCase): @@ -14,6 +14,7 @@ class TestAPIBase(unittest.TestCase): def setUpClass(cls): print("\n---------------------\nSetting up API base tests...\n") + @test_vcr.use_cassette def test_http_error_handler(self): status_code = None try: diff --git a/tests/test_genius.py b/tests/test_genius.py index 717e4864..ffd48eea 100644 --- a/tests/test_genius.py +++ b/tests/test_genius.py @@ -1,6 +1,8 @@ import os import unittest +import vcr + from lyricsgenius import Genius @@ -10,16 +12,27 @@ "Must declare environment variable: GENIUS_ACCESS_TOKEN") genius = Genius(access_token, sleep_time=1.0, timeout=15) +test_vcr = vcr.VCR( + path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml', + cassette_library_dir='tests/fixtures/cassettes', + filter_headers=['authorization'] +) + class TestEndpoints(unittest.TestCase): @classmethod + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix(' endpoints.yaml'), + serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Endpoint tests...\n") cls.search_term = "Ezra Furman" cls.song_title_only = "99 Problems" cls.tag = genius.tag('pop') + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml') def test_search_song(self): artist = "Jay-Z" # Empty response @@ -50,6 +63,7 @@ def test_search_song(self): response = genius.search_song(self.song_title_only, artist="Drake") self.assertFalse(response.title.lower() == self.song_title_only.lower()) + @test_vcr.use_cassette def test_song_annotations(self): msg = "Incorrect song annotation response." r = sorted(genius.song_annotations(1)) @@ -95,10 +109,14 @@ def setUpClass(cls): "\nI wonder who you’re thinking of" ) + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml') def test_lyrics_with_url(self): lyrics = genius.lyrics(self.song_url) self.assertTrue(lyrics.endswith(self.lyrics_ending)) + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml') def test_lyrics_with_id(self): lyrics = genius.lyrics(self.song_id) self.assertTrue(lyrics.endswith(self.lyrics_ending)) diff --git a/tests/test_public_methods.py b/tests/test_public_methods.py index 6a260b59..d7e504fd 100644 --- a/tests/test_public_methods.py +++ b/tests/test_public_methods.py @@ -1,7 +1,14 @@ import unittest +import vcr + from lyricsgenius import PublicAPI +try: + from .test_genius import test_vcr +except ModuleNotFoundError: + from test_genius import test_vcr + client = PublicAPI() @@ -12,31 +19,37 @@ def setUpClass(cls): print("\n---------------------\nSetting up album methods tests...\n") cls.album_id = 104614 + @test_vcr.use_cassette def test_album(self): msg = "Album ID did not match." r = client.album(self.album_id) self.assertEqual(r['album']['id'], self.album_id, msg) + @test_vcr.use_cassette def test_albums_charts(self): msg = "Album charts were empty." r = client.albums_charts() self.assertTrue("chart_items" in r, msg) + @test_vcr.use_cassette def test_album_comments(self): msg = "Album comments were empty." r = client.album_comments(self.album_id) self.assertTrue("comments" in r, msg) + @test_vcr.use_cassette def test_album_cover_arts(self): msg = "Album cover arts were empty." r = client.album_cover_arts(self.album_id) self.assertTrue("cover_arts" in r, msg) + @test_vcr.use_cassette def test_album_leaderboard(self): msg = "Album leaderboard was empty." r = client.album_leaderboard(self.album_id) self.assertTrue("leaderboard" in r, msg) + @test_vcr.use_cassette def test_album_tracks(self): msg = "Album tracks were empty." r = client.album_tracks(self.album_id) @@ -50,16 +63,20 @@ def setUpClass(cls): print("\n---------------------\nSetting up annotation methods tests...\n") cls.annotation_id = 10225840 + @test_vcr.use_cassette( + path_transformer=vcr.VCR.ensure_suffix(' public' + '.' + test_vcr.serializer)) def test_annotation(self): msg = "annotation ID did not match." r = client.annotation(self.annotation_id) self.assertEqual(r['annotation']['id'], self.annotation_id, msg) + @test_vcr.use_cassette def test_annotation_edits(self): msg = "annotation edits were empty." r = client.annotation_edits(self.annotation_id) self.assertTrue("versions" in r, msg) + @test_vcr.use_cassette def test_annotation_comments(self): msg = "annotation comments were empty." r = client.annotation_comments(self.annotation_id) @@ -73,16 +90,19 @@ def setUpClass(cls): print("\n---------------------\nSetting up article methods tests...\n") cls.article_id = 11880 + @test_vcr.use_cassette def test_article(self): msg = "article ID did not match." r = client.article(self.article_id) self.assertEqual(r['article']['id'], self.article_id, msg) + @test_vcr.use_cassette def test_article_comments(self): msg = "article comments were empty." r = client.article_comments(self.article_id) self.assertTrue("comments" in r, msg) + @test_vcr.use_cassette def test_latest_articles(self): msg = "latest articles were empty." r = client.latest_articles() @@ -96,34 +116,42 @@ def setUpClass(cls): print("\n---------------------\nSetting up artist methods tests...\n") cls.artist_id = 1665 + @test_vcr.use_cassette def test_artist(self): r = client.artist(self.artist_id) self.assertEqual(r['artist']['id'], self.artist_id) + @test_vcr.use_cassette def test_artist_activity(self): r = client.artist_activity(self.artist_id) self.assertTrue("line_items" in r) + @test_vcr.use_cassette def test_artist_albums(self): r = client.artist_albums(self.artist_id) self.assertTrue("albums" in r) + # @test_vcr.use_cassette # def test_artist_contribution_opportunities(self): # r = client.artist_contribution_opportunities(self.artist_id) # self.assertIsNotNone(r.get('contribution_opportunities')) + @test_vcr.use_cassette def test_artist_followers(self): r = client.artist_followers(self.artist_id) self.assertTrue("followers" in r) + @test_vcr.use_cassette def test_artist_leaderboard(self): r = client.artist_leaderboard(self.artist_id) self.assertTrue("leaderboard" in r) + @test_vcr.use_cassette def test_artist_songs(self): r = client.artist_songs(self.artist_id) self.assertTrue("songs" in r) + @test_vcr.use_cassette def test_search_artist_songs(self): r = client.search_artist_songs(self.artist_id, 'test') self.assertTrue("songs" in r) @@ -136,6 +164,7 @@ def setUpClass(cls): print("\n---------------------\nSetting up cover arts methods tests...\n") cls.album_id = 104614 + @test_vcr.use_cassette def test_cover_arts(self): r = client.cover_arts(self.album_id) self.assertTrue("cover_arts" in r) @@ -148,14 +177,17 @@ def setUpClass(cls): print("\n---------------------\nSetting up discussion methods tests...\n") # cls.discussion_id = 123 # +# @test_vcr.use_cassette # def test_discussion(self): # r = client.discussion(self.discussion_id) # self.assertEqual(r['discussion']['id'], self.discussion_id) # +# @test_vcr.use_cassette # def test_discussion_replies(self): # r = client.discussion_replies(self.discussion_id) # self.assertTrue("forum_posts" in r) + @test_vcr.use_cassette def test_discussions(self): r = client.discussions() self.assertTrue("discussions" in r) @@ -167,10 +199,12 @@ class TestLeaderboardMethods(unittest.TestCase): def setUpClass(cls): print("\n---------------------\nSetting up leaerboard methods tests...\n") + @test_vcr.use_cassette def test_leaderboard(self): r = client.leaderboard() self.assertTrue("leaderboard" in r) + @test_vcr.use_cassette def test_charts(self): r = client.charts() self.assertTrue("chart_items" in r) @@ -183,6 +217,7 @@ def setUpClass(cls): print("\n---------------------\nSetting up question methods tests...\n") cls.album_id = 104614 + @test_vcr.use_cassette def test_questions(self): r = client.questions(self.album_id) self.assertIsNotNone(r.get('questions')) @@ -196,11 +231,13 @@ def setUpClass(cls): cls.web_page_id = 10347 cls.referent_ids = [20793764, 20641014] + @test_vcr.use_cassette def test_referent(self): r = client.referent(self.referent_ids) self.assertTrue(str(self.referent_ids[0]) in r['referents']) self.assertTrue(str(self.referent_ids[1]) in r['referents']) + @test_vcr.use_cassette def test_referents(self): r = client.referents(web_page_id=self.web_page_id) self.assertIsNotNone(r.get('referents')) @@ -213,38 +250,47 @@ def setUpClass(cls): print("\n---------------------\nSetting up search methods tests...\n") cls.search_term = 'test' + @test_vcr.use_cassette def test_search(self): r = client.search(self.search_term) self.assertIsNotNone(r['hits']) + @test_vcr.use_cassette def test_search_albums(self): r = client.search_albums(self.search_term) self.assertEqual(r['sections'][0]['type'], 'album') + @test_vcr.use_cassette def test_search_articles(self): r = client.search_articles(self.search_term) self.assertEqual(r['sections'][0]['type'], 'article') + @test_vcr.use_cassette def test_search_artists(self): r = client.search_artists(self.search_term) self.assertEqual(r['sections'][0]['type'], 'artist') + @test_vcr.use_cassette def test_search_lyrics(self): r = client.search_lyrics(self.search_term) self.assertEqual(r['sections'][0]['type'], 'lyric') + @test_vcr.use_cassette def test_search_songs(self): r = client.search_songs(self.search_term) self.assertEqual(r['sections'][0]['type'], 'song') + @test_vcr.use_cassette def test_search_users(self): r = client.search_users(self.search_term) self.assertEqual(r['sections'][0]['type'], 'user') + @test_vcr.use_cassette def test_search_videos(self): r = client.search_videos(self.search_term) self.assertEqual(r['sections'][0]['type'], 'video') + @test_vcr.use_cassette def test_search_all(self): r = client.search_all(self.search_term) self.assertEqual(r['sections'][0]['type'], 'top_hit') @@ -257,18 +303,22 @@ def setUpClass(cls): print("\n---------------------\nSetting up song methods tests...\n") cls.song_id = 378195 + @test_vcr.use_cassette def test_song(self): r = client.song(self.song_id) self.assertEqual(r['song']['id'], self.song_id) + @test_vcr.use_cassette def test_song_activity(self): r = client.song_activity(self.song_id) self.assertTrue("line_items" in r) + @test_vcr.use_cassette def test_song_comments(self): r = client.song_comments(self.song_id) self.assertTrue("comments" in r) + @test_vcr.use_cassette def test_song_contributors(self): r = client.song_contributors(self.song_id) self.assertTrue("contributors" in r) @@ -281,56 +331,68 @@ def setUpClass(cls): print("\n---------------------\nSetting up user methods tests...\n") cls.user_id = 1 + @test_vcr.use_cassette def test_user(self): r = client.user(self.user_id) self.assertEqual(r['user']['id'], self.user_id) + @test_vcr.use_cassette def test_user_accomplishments(self): r = client.user_accomplishments(self.user_id) self.assertTrue("accomplishments" in r) + @test_vcr.use_cassette def test_user_following(self): r = client.user_following(self.user_id) self.assertTrue("followed_users" in r) + @test_vcr.use_cassette def test_user_followers(self): r = client.user_followers(self.user_id) self.assertTrue("followers" in r) + @test_vcr.use_cassette def test_user_contributions(self): r = client.user_contributions(self.user_id) self.assertTrue("contribution_groups" in r) + @test_vcr.use_cassette def test_user_annotations(self): r = client.user_annotations(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'annotation') + @test_vcr.use_cassette def test_user_articles(self): r = client.user_articles(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'article') + @test_vcr.use_cassette def test_user_pyongs(self): r = client.user_pyongs(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'pyong') + @test_vcr.use_cassette def test_user_questions_and_answers(self): r = client.user_questions_and_answers(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'answer') + @test_vcr.use_cassette def test_user_suggestions(self): r = client.user_suggestions(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'comment') + @test_vcr.use_cassette def test_user_transcriptions(self): r = client.user_transcriptions(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'song') + @test_vcr.use_cassette def test_user_unreviewed(self): r = client.user_unreviewed(self.user_id) type = r['contribution_groups'][0]['contribution_type'] @@ -344,10 +406,12 @@ def setUpClass(cls): print("\n---------------------\nSetting up video methods tests...\n") cls.video_id = 18681 + @test_vcr.use_cassette def test_video(self): r = client.video(self.video_id) self.assertEqual(r['video']['id'], self.video_id) + @test_vcr.use_cassette def test_videos(self): r = client.videos(video_id=self.video_id, series=True) self.assertTrue("video_lists" in r) @@ -364,10 +428,12 @@ def setUpClass(cls): # cls.line_item_id = 146262999 cls.annotation_id = 10225840 + # @test_vcr.use_cassette # def test_line_item(self): # r = client.line_item(self.line_item_id) # self.assertTrue("line_item" in r) + @test_vcr.use_cassette def test_voters(self): r = client.voters(annotation_id=self.annotation_id) self.assertTrue("voters" in r) diff --git a/tests/test_song.py b/tests/test_song.py index edbbe593..2c8b406e 100644 --- a/tests/test_song.py +++ b/tests/test_song.py @@ -1,10 +1,12 @@ import os import unittest +import vcr + try: - from .test_genius import genius + from .test_genius import genius, test_vcr except ModuleNotFoundError: - from test_genius import genius + from test_genius import genius, test_vcr from lyricsgenius.types import Song from lyricsgenius.utils import clean_str @@ -12,6 +14,8 @@ class TestSong(unittest.TestCase): @classmethod + @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix(' song.yaml'), + serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Song tests...\n") cls.artist_name = 'Andy Shauf' @@ -35,14 +39,6 @@ def test_artist(self): # The returned artist name does not match the artist of the requested song. self.assertEqual(self.song.artist, self.artist_name) - # def test_album(self): - # msg = "The returned album name does not match the album of the requested song." - # self.assertEqual(self.song.album, self.album, msg) - - # def test_year(self): - # msg = "The returned year does not match the year of the requested song" - # self.assertEqual(self.song.year, self.year, msg) - def test_lyrics_raw(self): lyrics = '[Verse 1: Andy Shauf]' self.assertTrue(self.song.lyrics.startswith(lyrics)) @@ -51,17 +47,9 @@ def test_lyrics_no_section_headers(self): lyrics = 'Begin again\nThis time you should take a bow at the' self.assertTrue(self.song_trimmed.lyrics.startswith(lyrics)) - # def test_media(self): - # msg = "The returned song does not have a media attribute." - # self.assertTrue(hasattr(self.song, 'media'), msg) - def test_result_is_lyrics(self): self.assertTrue(genius._result_is_lyrics(self.song.to_dict())) - # def test_producer_artists(self): - # # Producer artist should be 'Andy Shauf'. - # self.assertEqual(self.song.producer_artists[0]["name"], "Andy Shauf") - def test_saving_json_file(self): print('\n') extension = 'json' From 92a3d694bf72ea7a1822192167b09982caa03360 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 8 Nov 2020 14:36:17 +0330 Subject: [PATCH 03/44] changed displaying types attributes as table in docs --- docs/src/reference/types.rst | 145 +++++++++++++++++++++++++++++++++++ lyricsgenius/types/album.py | 18 +---- lyricsgenius/types/artist.py | 17 +--- lyricsgenius/types/song.py | 27 +------ 4 files changed, 148 insertions(+), 59 deletions(-) diff --git a/docs/src/reference/types.rst b/docs/src/reference/types.rst index bfcbfbbf..75e16c88 100644 --- a/docs/src/reference/types.rst +++ b/docs/src/reference/types.rst @@ -36,6 +36,50 @@ Album ------ An album from Genius that has the album's songs and their lyrics. +Attributes +^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + + * - Attribute + - Type + + * - _type + - :obj:`str` + + * - api_path + - :obj:`str` + + * - artist + - :class:`Artist` + + * - cover_art_thumbnail_url + - :obj:`str` + + * - cover_art_url + - :obj:`str` + + * - full_title + - :obj:`str` + + * - id + - :obj:`int` + + * - name + - :obj:`str` + + * - name_with_artist + - :obj:`str` + + * - release_date_components + - :class:`datetime` + + * - songs + - :obj:`list` + + * - url + - :obj:`str` + Methods ^^^^^^^^ @@ -59,6 +103,41 @@ Artist The Artist object which holds the details of the artist and the `Song`_ objects of that artist. +Attributes +^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + + * - Attribute + - Type + + + * - api_path + - :obj:`str` + + * - header_image_url + - :obj:`str` + + * - id + - :obj:`int` + + * - image_url + - :obj:`str` + + * - is_meme_verified + - :obj:`bool` + + * - is_verified + - :obj:`bool` + + * - name + - :obj:`str` + + * - songs + - :obj:`list` + + * - url + - :obj:`str` Methods ^^^^^^^^ @@ -83,6 +162,72 @@ Song ---- This is the Song object which holds the details of the song. +Attributes +^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + + * - Attribute + - Type + + + * - annotation_count + - :obj:`int` + + * - api_path + - :obj:`str` + + * - artist + - :obj:`str` + + * - full_title + - :obj:`str` + + * - header_image_thumbnail_url + - :obj:`str` + + * - header_image_url + - :obj:`str` + + * - id + - :obj:`int` + + * - lyrics + - :obj:`str` + + * - lyrics_owner_id + - :obj:`int` + + * - lyrics_state + - :obj:`str` + + * - path + - :obj:`str` + + * - primary_artist + - :class:`Artist` + + * - pyongs_count + - :obj:`int` + + * - song_art_image_thumbnail_url + - :obj:`str` + + * - song_art_image_url + - :obj:`str` + + * - stats + - :class:`Stats` + + * - title + - :obj:`str` + + * - title_with_featured + - :obj:`str` + + * - url + - :obj:`str` + Methods ^^^^^^^^ .. autosummary:: diff --git a/lyricsgenius/types/album.py b/lyricsgenius/types/album.py index 30cf23c4..3de47835 100644 --- a/lyricsgenius/types/album.py +++ b/lyricsgenius/types/album.py @@ -4,23 +4,7 @@ class Album(BaseEntity): - """An album from the Genius.com database. - - Attributes: - _type (:obj:`str`) - api_path (:obj:`str`) - artist (:class:`Artist`) - cover_art_thumbnail_url (:obj:`str`) - cover_art_url (:obj:`str`) - full_title (:obj:`str`) - id (:obj:`int`) - name (:obj:`str`) - name_with_artist (:obj:`str`) - release_date_components (:class:`datetime`) - songs (:obj:`list`): - A list of :class:`Song` objects. - url (:obj:`str`) - """ + """An album from the Genius.com database.""" def __init__(self, client, json_dict, songs): body = json_dict diff --git a/lyricsgenius/types/artist.py b/lyricsgenius/types/artist.py index f3dce53c..0c4350da 100644 --- a/lyricsgenius/types/artist.py +++ b/lyricsgenius/types/artist.py @@ -9,22 +9,7 @@ class Artist(BaseEntity): - """An artist with songs from the Genius.com database. - - Attributes: - api_path (:obj:`str`) - header_image_url (:obj:`str`) - id (:obj:`int`) - image_url (:obj:`str`) - is_meme_verified (:obj:`bool`) - is_verified (:obj:`bool`) - name (:obj:`str`) - songs (:obj:`list`): - A list of :class:`Song` objects - or an empty list. - url (:obj:`str`) - - """ + """An artist with songs from the Genius.com database.""" def __init__(self, client, json_dict): # Artist Constructor diff --git a/lyricsgenius/types/song.py b/lyricsgenius/types/song.py index b7c8e8df..bb0e5a25 100644 --- a/lyricsgenius/types/song.py +++ b/lyricsgenius/types/song.py @@ -9,32 +9,7 @@ class Song(BaseEntity): - """A song from the Genius.com database. - - Attributes: - annotation_count (:obj:`int`) - api_path (:obj:`str`) - artist (:obj:`str`): - Primary artist's name - (Same as ``Song.primary_artist.name``) - full_title (:obj:`str`) - header_image_thumbnail_url (:obj:`str`) - header_image_url (:obj:`str`) - id (:obj:`int`) - lyrics (:obj:`str`) - lyrics_owner_id (:obj:`int`) - lyrics_state (:obj:`str`) - path (:obj:`str`) - primary_artist (:class:`Artist`) - pyongs_count (:obj:`int`) - song_art_image_thumbnail_url (:obj:`str`) - song_art_image_url (:obj:`str`) - stats (:class:`Stats`) - title (:obj:`str`) - title_with_featured (:obj:`str`) - url (:obj:`str`) - - """ + """A song from the Genius.com database.""" def __init__(self, client, json_dict, lyrics=""): body = json_dict From 92816fd5c4d315ceba8912c1c48f84c827642e52 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 8 Nov 2020 15:28:06 +0330 Subject: [PATCH 04/44] add vcrpy to requirements --- setup.py | 5 ++++- tox.ini | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7f9c012c..9db79042 100644 --- a/setup.py +++ b/setup.py @@ -35,10 +35,13 @@ 'flake8', 'flake8-bugbear', 'pygments', + ], + 'tests': [ + 'vcrpy~=4.1' ] } extras_require['dev'] = ( - extras_require['docs'] + extras_require['checks'] + extras_require['docs'] + extras_require['checks'] + extras_require['tests'] ) setup( diff --git a/tox.ini b/tox.ini index a783badf..2bc0c2e5 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,8 @@ passenv = GENIUS* [testenv:test] ; Inherit everything from testenv +extras = tests + [testenv:docs] description = Build Sphinx HTML documentation From 33f096c9233ac07b059857c85de58345651bab3a Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 8 Nov 2020 15:39:19 +0330 Subject: [PATCH 05/44] test tox.ini --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2bc0c2e5..43ecca84 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,6 @@ passenv = GENIUS* [testenv:test] ; Inherit everything from testenv -extras = tests [testenv:docs] From cfe657e0dc34db5873f6466d202f45098b117b98 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 8 Nov 2020 15:40:45 +0330 Subject: [PATCH 06/44] Revert "test tox.ini" This reverts commit 33f096c9233ac07b059857c85de58345651bab3a. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 43ecca84..2bc0c2e5 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,7 @@ passenv = GENIUS* [testenv:test] ; Inherit everything from testenv +extras = tests [testenv:docs] From 99bf0096073f7a19b8ed81616f123515fae97fbb Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Mon, 9 Nov 2020 23:08:58 +0330 Subject: [PATCH 07/44] removed except in tests --- tests/__init__.py | 3 +++ tests/test_album.py | 5 +---- tests/test_api.py | 5 +---- tests/test_artist.py | 5 +---- tests/test_base.py | 5 +---- tests/test_public_methods.py | 6 ++---- tests/test_song.py | 5 +---- tests/test_utils.py | 6 ++---- 8 files changed, 12 insertions(+), 28 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..2bcece0b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +import sys + +sys.path.append('.') \ No newline at end of file diff --git a/tests/test_album.py b/tests/test_album.py index f5d8e013..ddaa8164 100644 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -3,10 +3,7 @@ import vcr -try: - from .test_genius import genius, test_vcr -except ModuleNotFoundError: - from test_genius import genius, test_vcr +from .test_genius import genius, test_vcr from lyricsgenius.types import Album diff --git a/tests/test_api.py b/tests/test_api.py index bdd2e6c2..5109fbe9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,6 @@ import unittest -try: - from .test_genius import genius, test_vcr -except ModuleNotFoundError: - from test_genius import genius, test_vcr +from .test_genius import genius, test_vcr class TestAPI(unittest.TestCase): diff --git a/tests/test_artist.py b/tests/test_artist.py index e4dce9e7..b30bda55 100644 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -3,10 +3,7 @@ import vcr -try: - from .test_genius import genius, test_vcr -except ModuleNotFoundError: - from test_genius import genius, test_vcr +from .test_genius import genius, test_vcr from lyricsgenius.types import Artist from lyricsgenius.utils import sanitize_filename diff --git a/tests/test_base.py b/tests/test_base.py index 31dabed8..d38b3806 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -2,10 +2,7 @@ from requests.exceptions import HTTPError -try: - from .test_genius import genius, test_vcr -except ModuleNotFoundError: - from test_genius import genius, test_vcr +from .test_genius import genius, test_vcr class TestAPIBase(unittest.TestCase): diff --git a/tests/test_public_methods.py b/tests/test_public_methods.py index d7e504fd..be4b8611 100644 --- a/tests/test_public_methods.py +++ b/tests/test_public_methods.py @@ -4,10 +4,8 @@ from lyricsgenius import PublicAPI -try: - from .test_genius import test_vcr -except ModuleNotFoundError: - from test_genius import test_vcr +from .test_genius import test_vcr + client = PublicAPI() diff --git a/tests/test_song.py b/tests/test_song.py index 2c8b406e..e1a101e8 100644 --- a/tests/test_song.py +++ b/tests/test_song.py @@ -3,10 +3,7 @@ import vcr -try: - from .test_genius import genius, test_vcr -except ModuleNotFoundError: - from test_genius import genius, test_vcr +from .test_genius import genius, test_vcr from lyricsgenius.types import Song from lyricsgenius.utils import clean_str diff --git a/tests/test_utils.py b/tests/test_utils.py index 6f6eb1cd..8221d702 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,10 +5,8 @@ sanitize_filename, auth_from_environment ) -try: - from .test_genius import genius -except ModuleNotFoundError: - from test_genius import genius + +from .test_genius import genius class TestUtils(unittest.TestCase): From 1d9f29754f737d755d5288c3b1acd5099a12b508 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 10 Nov 2020 00:15:00 +0330 Subject: [PATCH 08/44] merge pr/109 into docs --- .gitignore | 7 ------- setup.py | 2 +- tests/__init__.py | 3 --- tests/test_genius.py | 7 ++++++- tox.ini | 1 - 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 4f4fa9c8..875743d8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,10 +28,3 @@ __pycache__ dist build .idea - -# VCR cache files -test_get_annotation -test_get_referents_web_page -test_get_song_annotations -test_search_genius_web -test_search_song \ No newline at end of file diff --git a/setup.py b/setup.py index 9db79042..c4869c96 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ extras_require = { 'docs': [ - 'sphinx~=3.2', + 'sphinx~=3.3', 'sphinx-rtd-theme', ], 'checks': [ diff --git a/tests/__init__.py b/tests/__init__.py index 2bcece0b..e69de29b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +0,0 @@ -import sys - -sys.path.append('.') \ No newline at end of file diff --git a/tests/test_genius.py b/tests/test_genius.py index ffd48eea..4c8aaac3 100644 --- a/tests/test_genius.py +++ b/tests/test_genius.py @@ -12,10 +12,15 @@ "Must declare environment variable: GENIUS_ACCESS_TOKEN") genius = Genius(access_token, sleep_time=1.0, timeout=15) +cassettes_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'fixtures/cassettes' +) + test_vcr = vcr.VCR( path_transformer=vcr.VCR.ensure_suffix('.yaml'), serializer='yaml', - cassette_library_dir='tests/fixtures/cassettes', + cassette_library_dir=cassettes_path, filter_headers=['authorization'] ) diff --git a/tox.ini b/tox.ini index 2bc0c2e5..a0c750f4 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,6 @@ passenv = GENIUS* ; Inherit everything from testenv extras = tests - [testenv:docs] description = Build Sphinx HTML documentation extras = docs From c1221ae4ca0ab37541c2f09e2266526bd15b9bab Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 10 Nov 2020 14:33:58 +0330 Subject: [PATCH 09/44] merge pr/109 into docs --- docs/src/reference/types.rst | 145 ----------------------------------- lyricsgenius/types/album.py | 18 ++++- lyricsgenius/types/artist.py | 17 +++- lyricsgenius/types/song.py | 27 ++++++- tests/__init__.py | 27 +++++++ tests/test_album.py | 4 +- tests/test_api.py | 3 +- tests/test_artist.py | 4 +- tests/test_auth.py | 1 + tests/test_base.py | 2 +- tests/test_genius.py | 24 +----- tests/test_public_methods.py | 16 +++- tests/test_song.py | 4 +- tests/test_utils.py | 2 +- 14 files changed, 118 insertions(+), 176 deletions(-) diff --git a/docs/src/reference/types.rst b/docs/src/reference/types.rst index 75e16c88..bfcbfbbf 100644 --- a/docs/src/reference/types.rst +++ b/docs/src/reference/types.rst @@ -36,50 +36,6 @@ Album ------ An album from Genius that has the album's songs and their lyrics. -Attributes -^^^^^^^^^^ -.. list-table:: - :header-rows: 1 - - * - Attribute - - Type - - * - _type - - :obj:`str` - - * - api_path - - :obj:`str` - - * - artist - - :class:`Artist` - - * - cover_art_thumbnail_url - - :obj:`str` - - * - cover_art_url - - :obj:`str` - - * - full_title - - :obj:`str` - - * - id - - :obj:`int` - - * - name - - :obj:`str` - - * - name_with_artist - - :obj:`str` - - * - release_date_components - - :class:`datetime` - - * - songs - - :obj:`list` - - * - url - - :obj:`str` - Methods ^^^^^^^^ @@ -103,41 +59,6 @@ Artist The Artist object which holds the details of the artist and the `Song`_ objects of that artist. -Attributes -^^^^^^^^^^ -.. list-table:: - :header-rows: 1 - - * - Attribute - - Type - - - * - api_path - - :obj:`str` - - * - header_image_url - - :obj:`str` - - * - id - - :obj:`int` - - * - image_url - - :obj:`str` - - * - is_meme_verified - - :obj:`bool` - - * - is_verified - - :obj:`bool` - - * - name - - :obj:`str` - - * - songs - - :obj:`list` - - * - url - - :obj:`str` Methods ^^^^^^^^ @@ -162,72 +83,6 @@ Song ---- This is the Song object which holds the details of the song. -Attributes -^^^^^^^^^^ -.. list-table:: - :header-rows: 1 - - * - Attribute - - Type - - - * - annotation_count - - :obj:`int` - - * - api_path - - :obj:`str` - - * - artist - - :obj:`str` - - * - full_title - - :obj:`str` - - * - header_image_thumbnail_url - - :obj:`str` - - * - header_image_url - - :obj:`str` - - * - id - - :obj:`int` - - * - lyrics - - :obj:`str` - - * - lyrics_owner_id - - :obj:`int` - - * - lyrics_state - - :obj:`str` - - * - path - - :obj:`str` - - * - primary_artist - - :class:`Artist` - - * - pyongs_count - - :obj:`int` - - * - song_art_image_thumbnail_url - - :obj:`str` - - * - song_art_image_url - - :obj:`str` - - * - stats - - :class:`Stats` - - * - title - - :obj:`str` - - * - title_with_featured - - :obj:`str` - - * - url - - :obj:`str` - Methods ^^^^^^^^ .. autosummary:: diff --git a/lyricsgenius/types/album.py b/lyricsgenius/types/album.py index 3de47835..30cf23c4 100644 --- a/lyricsgenius/types/album.py +++ b/lyricsgenius/types/album.py @@ -4,7 +4,23 @@ class Album(BaseEntity): - """An album from the Genius.com database.""" + """An album from the Genius.com database. + + Attributes: + _type (:obj:`str`) + api_path (:obj:`str`) + artist (:class:`Artist`) + cover_art_thumbnail_url (:obj:`str`) + cover_art_url (:obj:`str`) + full_title (:obj:`str`) + id (:obj:`int`) + name (:obj:`str`) + name_with_artist (:obj:`str`) + release_date_components (:class:`datetime`) + songs (:obj:`list`): + A list of :class:`Song` objects. + url (:obj:`str`) + """ def __init__(self, client, json_dict, songs): body = json_dict diff --git a/lyricsgenius/types/artist.py b/lyricsgenius/types/artist.py index 0c4350da..f3dce53c 100644 --- a/lyricsgenius/types/artist.py +++ b/lyricsgenius/types/artist.py @@ -9,7 +9,22 @@ class Artist(BaseEntity): - """An artist with songs from the Genius.com database.""" + """An artist with songs from the Genius.com database. + + Attributes: + api_path (:obj:`str`) + header_image_url (:obj:`str`) + id (:obj:`int`) + image_url (:obj:`str`) + is_meme_verified (:obj:`bool`) + is_verified (:obj:`bool`) + name (:obj:`str`) + songs (:obj:`list`): + A list of :class:`Song` objects + or an empty list. + url (:obj:`str`) + + """ def __init__(self, client, json_dict): # Artist Constructor diff --git a/lyricsgenius/types/song.py b/lyricsgenius/types/song.py index bb0e5a25..b7c8e8df 100644 --- a/lyricsgenius/types/song.py +++ b/lyricsgenius/types/song.py @@ -9,7 +9,32 @@ class Song(BaseEntity): - """A song from the Genius.com database.""" + """A song from the Genius.com database. + + Attributes: + annotation_count (:obj:`int`) + api_path (:obj:`str`) + artist (:obj:`str`): + Primary artist's name + (Same as ``Song.primary_artist.name``) + full_title (:obj:`str`) + header_image_thumbnail_url (:obj:`str`) + header_image_url (:obj:`str`) + id (:obj:`int`) + lyrics (:obj:`str`) + lyrics_owner_id (:obj:`int`) + lyrics_state (:obj:`str`) + path (:obj:`str`) + primary_artist (:class:`Artist`) + pyongs_count (:obj:`int`) + song_art_image_thumbnail_url (:obj:`str`) + song_art_image_url (:obj:`str`) + stats (:class:`Stats`) + title (:obj:`str`) + title_with_featured (:obj:`str`) + url (:obj:`str`) + + """ def __init__(self, client, json_dict, lyrics=""): body = json_dict diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..1f97eaf2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,27 @@ +import os + +import vcr + +from lyricsgenius import Genius + + +# Import client access token from environment variable +access_token = os.environ.get("GENIUS_ACCESS_TOKEN", None) +assert access_token is not None, ( + "Must declare environment variable: GENIUS_ACCESS_TOKEN") + +# Genius client +genius = Genius(access_token, sleep_time=1.0, timeout=15) + +cassettes_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'fixtures/cassettes' +) + +# VCR Configs +test_vcr = vcr.VCR( + path_transformer=vcr.VCR.ensure_suffix('.yaml'), + serializer='yaml', + cassette_library_dir=cassettes_path, + filter_headers=['authorization'] +) diff --git a/tests/test_album.py b/tests/test_album.py index ddaa8164..280d1ca3 100644 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -1,9 +1,10 @@ import unittest import os +import warnings import vcr -from .test_genius import genius, test_vcr +from . import genius, test_vcr from lyricsgenius.types import Album @@ -14,6 +15,7 @@ class TestAlbum(unittest.TestCase): serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Album tests...\n") + warnings.simplefilter("ignore", ResourceWarning) cls.album_name = "The Party" cls.artist_name = "Andy Shauf" cls.num_songs = 10 diff --git a/tests/test_api.py b/tests/test_api.py index 5109fbe9..9711bd66 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,7 @@ import unittest -from .test_genius import genius, test_vcr + +from . import genius, test_vcr class TestAPI(unittest.TestCase): diff --git a/tests/test_artist.py b/tests/test_artist.py index b30bda55..a830e4f5 100644 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -1,9 +1,10 @@ import unittest import os + import vcr -from .test_genius import genius, test_vcr +from . import genius, test_vcr from lyricsgenius.types import Artist from lyricsgenius.utils import sanitize_filename @@ -15,6 +16,7 @@ class TestArtist(unittest.TestCase): serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Artist tests...\n") + cls.artist_name = "The Beatles" cls.new_song = "Paperback Writer" cls.max_songs = 2 diff --git a/tests/test_auth.py b/tests/test_auth.py index fe359a67..2bac4573 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import MagicMock, patch + from lyricsgenius import OAuth2 client_id = os.environ["GENIUS_CLIENT_ID"] diff --git a/tests/test_base.py b/tests/test_base.py index d38b3806..e52ec24d 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -2,7 +2,7 @@ from requests.exceptions import HTTPError -from .test_genius import genius, test_vcr +from . import genius, test_vcr class TestAPIBase(unittest.TestCase): diff --git a/tests/test_genius.py b/tests/test_genius.py index 4c8aaac3..1949f63c 100644 --- a/tests/test_genius.py +++ b/tests/test_genius.py @@ -1,28 +1,8 @@ -import os import unittest import vcr -from lyricsgenius import Genius - - -# Import client access token from environment variable -access_token = os.environ.get("GENIUS_ACCESS_TOKEN", None) -assert access_token is not None, ( - "Must declare environment variable: GENIUS_ACCESS_TOKEN") -genius = Genius(access_token, sleep_time=1.0, timeout=15) - -cassettes_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - 'fixtures/cassettes' -) - -test_vcr = vcr.VCR( - path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml', - cassette_library_dir=cassettes_path, - filter_headers=['authorization'] -) +from . import genius, test_vcr class TestEndpoints(unittest.TestCase): @@ -32,6 +12,7 @@ class TestEndpoints(unittest.TestCase): serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Endpoint tests...\n") + cls.search_term = "Ezra Furman" cls.song_title_only = "99 Problems" cls.tag = genius.tag('pop') @@ -105,6 +86,7 @@ class TestLyrics(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up lyrics tests...\n") + cls.song_url = "https://genius.com/Andy-shauf-begin-again-lyrics" cls.song_id = 2885745 cls.lyrics_ending = ( diff --git a/tests/test_public_methods.py b/tests/test_public_methods.py index be4b8611..dcb96af7 100644 --- a/tests/test_public_methods.py +++ b/tests/test_public_methods.py @@ -1,10 +1,11 @@ import unittest + import vcr from lyricsgenius import PublicAPI -from .test_genius import test_vcr +from . import test_vcr client = PublicAPI() @@ -15,6 +16,7 @@ class TestAlbumMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up album methods tests...\n") + cls.album_id = 104614 @test_vcr.use_cassette @@ -59,6 +61,7 @@ class TestAnnotationMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up annotation methods tests...\n") + cls.annotation_id = 10225840 @test_vcr.use_cassette( @@ -86,6 +89,7 @@ class TestArticleMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up article methods tests...\n") + cls.article_id = 11880 @test_vcr.use_cassette @@ -112,6 +116,7 @@ class TestArtistMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up artist methods tests...\n") + cls.artist_id = 1665 @test_vcr.use_cassette @@ -160,6 +165,7 @@ class TestCoverArtMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up cover arts methods tests...\n") + cls.album_id = 104614 @test_vcr.use_cassette @@ -173,6 +179,7 @@ class TestDiscussionMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up discussion methods tests...\n") + # cls.discussion_id = 123 # # @test_vcr.use_cassette @@ -213,6 +220,7 @@ class TestQuestionMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up question methods tests...\n") + cls.album_id = 104614 @test_vcr.use_cassette @@ -226,6 +234,7 @@ class TestReferentMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up referent methods tests...\n") + cls.web_page_id = 10347 cls.referent_ids = [20793764, 20641014] @@ -246,6 +255,7 @@ class TestSearchMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up search methods tests...\n") + cls.search_term = 'test' @test_vcr.use_cassette @@ -299,6 +309,7 @@ class TestSongMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up song methods tests...\n") + cls.song_id = 378195 @test_vcr.use_cassette @@ -327,6 +338,7 @@ class TestUserMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up user methods tests...\n") + cls.user_id = 1 @test_vcr.use_cassette @@ -402,6 +414,7 @@ class TestVideoMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up video methods tests...\n") + cls.video_id = 18681 @test_vcr.use_cassette @@ -423,6 +436,7 @@ class TestMiscMethods(unittest.TestCase): @classmethod def setUpClass(cls): print("\n---------------------\nSetting up misc methods tests...\n") + # cls.line_item_id = 146262999 cls.annotation_id = 10225840 diff --git a/tests/test_song.py b/tests/test_song.py index e1a101e8..db26bc1b 100644 --- a/tests/test_song.py +++ b/tests/test_song.py @@ -1,9 +1,10 @@ import os import unittest + import vcr -from .test_genius import genius, test_vcr +from . import genius, test_vcr from lyricsgenius.types import Song from lyricsgenius.utils import clean_str @@ -15,6 +16,7 @@ class TestSong(unittest.TestCase): serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Song tests...\n") + cls.artist_name = 'Andy Shauf' cls.song_title = 'begin again' # Lowercase is intentional cls.album = 'The Party' diff --git a/tests/test_utils.py b/tests/test_utils.py index 8221d702..e666143d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,7 +6,7 @@ auth_from_environment ) -from .test_genius import genius +from . import genius class TestUtils(unittest.TestCase): From b77c6d16af951e74debed6b41d05551b50739d84 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 10 Nov 2020 14:53:46 +0330 Subject: [PATCH 10/44] - added checking for the instrumental attribute in song info - added a couple comments --- lyricsgenius/genius.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lyricsgenius/genius.py b/lyricsgenius/genius.py index 6b8e707d..f504d6fc 100644 --- a/lyricsgenius/genius.py +++ b/lyricsgenius/genius.py @@ -165,7 +165,8 @@ def _result_is_lyrics(self, song): 'interview', 'skit', 'instrumental', and 'setlist'. """ - if song['lyrics_state'] != 'complete': + if (song['lyrics_state'] != 'complete' + or song.get('instrumental')): return False expression = r"".join(["({})|".format(term) for term in self.excluded_terms]) @@ -321,6 +322,9 @@ def search_album(self, name=None, artist="", songs = [] next_page = 1 + + # It's unlikely for an album to have >=50 songs, + # but it's best to check while next_page: tracks = self.album_tracks(album_id=album_id, per_page=50, @@ -328,7 +332,8 @@ def search_album(self, name=None, artist="", text_format=text_format) for track in tracks['tracks']: song_info = track['song'] - if song_info['lyrics_state'] == 'complete': + if (song_info['lyrics_state'] == 'complete' + and not song_info.get('instrumental')): song_lyrics = self.lyrics(song_info['url']) else: song_lyrics = "" @@ -418,7 +423,9 @@ def search_song(self, title=None, artist="", song_id=None, if song_id is None and get_full_info is True: new_info = self.song(song_id)['song'] song_info.update(new_info) - if song_info['lyrics_state'] == 'complete': + + if (song_info['lyrics_state'] == 'complete' + and not song_info.get('instrumental')): lyrics = self.lyrics(song_info['url']) else: lyrics = "" @@ -546,7 +553,8 @@ def find_artist_id(search_term): continue # Create the Song object from lyrics and metadata - if song_info['lyrics_state'] == 'complete': + if (song_info['lyrics_state'] == 'complete' + and not song_info.get('instrumental')): lyrics = self.lyrics(song_info['url']) else: lyrics = "" @@ -689,6 +697,7 @@ def tag(self, name, page=None): ul = soup.find('ul', class_='song_list') for li in ul.find_all('li'): url = li.a.attrs['href'] + # Genius uses \xa0 in the HTML to add spaces song = [x.replace('\xa0', ' ') for x in li.a.span.stripped_strings] title = song[0] @@ -709,6 +718,7 @@ def tag(self, name, page=None): res = {'hits': hits} page = page if page is not None else 1 + # Full pages contain 20 items res['next_page'] = page + 1 if len(hits) == 20 else None return res From 5cb166835a23e2b3b1a39a15e750c68391ce8b90 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 10 Nov 2020 14:56:53 +0330 Subject: [PATCH 11/44] added instrumental attribute to types.Song --- lyricsgenius/types/song.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lyricsgenius/types/song.py b/lyricsgenius/types/song.py index b7c8e8df..210c9550 100644 --- a/lyricsgenius/types/song.py +++ b/lyricsgenius/types/song.py @@ -51,6 +51,7 @@ def __init__(self, client, json_dict, lyrics=""): self.full_title = body['full_title'] self.header_image_thumbnail_url = body['header_image_thumbnail_url'] self.header_image_url = body['header_image_url'] + self.instrumental = True if 'instrumental' in body else False self.lyrics_owner_id = body['lyrics_owner_id'] self.lyrics_state = body['lyrics_state'] self.path = body['path'] From 58cd38f942ac308b9ed66f5e07216dc3dd7c3330 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 10 Nov 2020 15:02:01 +0330 Subject: [PATCH 12/44] Revert "merge pr/109 into docs" This reverts commit c1221ae4ca0ab37541c2f09e2266526bd15b9bab. --- docs/src/reference/types.rst | 145 +++++++++++++++++++++++++++++++++++ lyricsgenius/types/album.py | 18 +---- lyricsgenius/types/artist.py | 17 +--- lyricsgenius/types/song.py | 27 +------ 4 files changed, 148 insertions(+), 59 deletions(-) diff --git a/docs/src/reference/types.rst b/docs/src/reference/types.rst index bfcbfbbf..75e16c88 100644 --- a/docs/src/reference/types.rst +++ b/docs/src/reference/types.rst @@ -36,6 +36,50 @@ Album ------ An album from Genius that has the album's songs and their lyrics. +Attributes +^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + + * - Attribute + - Type + + * - _type + - :obj:`str` + + * - api_path + - :obj:`str` + + * - artist + - :class:`Artist` + + * - cover_art_thumbnail_url + - :obj:`str` + + * - cover_art_url + - :obj:`str` + + * - full_title + - :obj:`str` + + * - id + - :obj:`int` + + * - name + - :obj:`str` + + * - name_with_artist + - :obj:`str` + + * - release_date_components + - :class:`datetime` + + * - songs + - :obj:`list` + + * - url + - :obj:`str` + Methods ^^^^^^^^ @@ -59,6 +103,41 @@ Artist The Artist object which holds the details of the artist and the `Song`_ objects of that artist. +Attributes +^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + + * - Attribute + - Type + + + * - api_path + - :obj:`str` + + * - header_image_url + - :obj:`str` + + * - id + - :obj:`int` + + * - image_url + - :obj:`str` + + * - is_meme_verified + - :obj:`bool` + + * - is_verified + - :obj:`bool` + + * - name + - :obj:`str` + + * - songs + - :obj:`list` + + * - url + - :obj:`str` Methods ^^^^^^^^ @@ -83,6 +162,72 @@ Song ---- This is the Song object which holds the details of the song. +Attributes +^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + + * - Attribute + - Type + + + * - annotation_count + - :obj:`int` + + * - api_path + - :obj:`str` + + * - artist + - :obj:`str` + + * - full_title + - :obj:`str` + + * - header_image_thumbnail_url + - :obj:`str` + + * - header_image_url + - :obj:`str` + + * - id + - :obj:`int` + + * - lyrics + - :obj:`str` + + * - lyrics_owner_id + - :obj:`int` + + * - lyrics_state + - :obj:`str` + + * - path + - :obj:`str` + + * - primary_artist + - :class:`Artist` + + * - pyongs_count + - :obj:`int` + + * - song_art_image_thumbnail_url + - :obj:`str` + + * - song_art_image_url + - :obj:`str` + + * - stats + - :class:`Stats` + + * - title + - :obj:`str` + + * - title_with_featured + - :obj:`str` + + * - url + - :obj:`str` + Methods ^^^^^^^^ .. autosummary:: diff --git a/lyricsgenius/types/album.py b/lyricsgenius/types/album.py index 30cf23c4..3de47835 100644 --- a/lyricsgenius/types/album.py +++ b/lyricsgenius/types/album.py @@ -4,23 +4,7 @@ class Album(BaseEntity): - """An album from the Genius.com database. - - Attributes: - _type (:obj:`str`) - api_path (:obj:`str`) - artist (:class:`Artist`) - cover_art_thumbnail_url (:obj:`str`) - cover_art_url (:obj:`str`) - full_title (:obj:`str`) - id (:obj:`int`) - name (:obj:`str`) - name_with_artist (:obj:`str`) - release_date_components (:class:`datetime`) - songs (:obj:`list`): - A list of :class:`Song` objects. - url (:obj:`str`) - """ + """An album from the Genius.com database.""" def __init__(self, client, json_dict, songs): body = json_dict diff --git a/lyricsgenius/types/artist.py b/lyricsgenius/types/artist.py index f3dce53c..0c4350da 100644 --- a/lyricsgenius/types/artist.py +++ b/lyricsgenius/types/artist.py @@ -9,22 +9,7 @@ class Artist(BaseEntity): - """An artist with songs from the Genius.com database. - - Attributes: - api_path (:obj:`str`) - header_image_url (:obj:`str`) - id (:obj:`int`) - image_url (:obj:`str`) - is_meme_verified (:obj:`bool`) - is_verified (:obj:`bool`) - name (:obj:`str`) - songs (:obj:`list`): - A list of :class:`Song` objects - or an empty list. - url (:obj:`str`) - - """ + """An artist with songs from the Genius.com database.""" def __init__(self, client, json_dict): # Artist Constructor diff --git a/lyricsgenius/types/song.py b/lyricsgenius/types/song.py index 210c9550..6a441d5d 100644 --- a/lyricsgenius/types/song.py +++ b/lyricsgenius/types/song.py @@ -9,32 +9,7 @@ class Song(BaseEntity): - """A song from the Genius.com database. - - Attributes: - annotation_count (:obj:`int`) - api_path (:obj:`str`) - artist (:obj:`str`): - Primary artist's name - (Same as ``Song.primary_artist.name``) - full_title (:obj:`str`) - header_image_thumbnail_url (:obj:`str`) - header_image_url (:obj:`str`) - id (:obj:`int`) - lyrics (:obj:`str`) - lyrics_owner_id (:obj:`int`) - lyrics_state (:obj:`str`) - path (:obj:`str`) - primary_artist (:class:`Artist`) - pyongs_count (:obj:`int`) - song_art_image_thumbnail_url (:obj:`str`) - song_art_image_url (:obj:`str`) - stats (:class:`Stats`) - title (:obj:`str`) - title_with_featured (:obj:`str`) - url (:obj:`str`) - - """ + """A song from the Genius.com database.""" def __init__(self, client, json_dict, lyrics=""): body = json_dict From 0471eb97046c23a4371fd2ec7d99a15e9a864e01 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 10 Nov 2020 15:04:17 +0330 Subject: [PATCH 13/44] add Song.instrumental to docs --- docs/src/reference/types.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/reference/types.rst b/docs/src/reference/types.rst index 75e16c88..7be72269 100644 --- a/docs/src/reference/types.rst +++ b/docs/src/reference/types.rst @@ -192,6 +192,9 @@ Attributes * - id - :obj:`int` + * - instrumental + - :obj:`bool` + * - lyrics - :obj:`str` From 2d1f63c89ea35a279ee427957a4669509f1f30d9 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 10 Nov 2020 15:18:53 +0330 Subject: [PATCH 14/44] - add instrumental to to_dict - use dict.get to determine instrumental value --- lyricsgenius/types/song.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lyricsgenius/types/song.py b/lyricsgenius/types/song.py index 6a441d5d..391b6d34 100644 --- a/lyricsgenius/types/song.py +++ b/lyricsgenius/types/song.py @@ -26,7 +26,7 @@ def __init__(self, client, json_dict, lyrics=""): self.full_title = body['full_title'] self.header_image_thumbnail_url = body['header_image_thumbnail_url'] self.header_image_url = body['header_image_url'] - self.instrumental = True if 'instrumental' in body else False + self.instrumental = body.get('instrumental', False) self.lyrics_owner_id = body['lyrics_owner_id'] self.lyrics_state = body['lyrics_state'] self.path = body['path'] @@ -41,6 +41,7 @@ def to_dict(self): body = super().to_dict() body['artist'] = self.artist body['lyrics'] = self.lyrics + body['instrumental'] = self.instrumental return body def to_json(self, From 0ba9c74917dde9b08af047c991878fbebf142fee Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 10 Nov 2020 15:28:37 +0330 Subject: [PATCH 15/44] set retries to 3 --- tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 1f97eaf2..a0138210 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,7 +11,7 @@ "Must declare environment variable: GENIUS_ACCESS_TOKEN") # Genius client -genius = Genius(access_token, sleep_time=1.0, timeout=15) +genius = Genius(access_token, sleep_time=1.0, timeout=15, retries=3) cassettes_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), From e131b0deb9619164d9a59ba02f292bf6634feb27 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Wed, 11 Nov 2020 17:12:34 +0330 Subject: [PATCH 16/44] - Genius: removed checking instrumental in search_artist (genius.artist_songs doesn't return that key in song info) - types.Song: removed Song.instrumental - clean_str(): change normalization method to NKFC - docs/snippets: changes in snippets titles --- docs/src/examples/snippets.rst | 8 ++++---- lyricsgenius/genius.py | 3 +-- lyricsgenius/types/song.py | 1 - lyricsgenius/utils.py | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/src/examples/snippets.rst b/docs/src/examples/snippets.rst index 2b02079a..add987b7 100644 --- a/docs/src/examples/snippets.rst +++ b/docs/src/examples/snippets.rst @@ -5,11 +5,11 @@ Snippets ================== Here are some snippets showcasing how the library can be used. -- `Authenticating using OAuth2`_ - `All the songs of an artist`_ - `Artist's least popular song`_ -- `Getting songs that have a tag` +- `Authenticating using OAuth2`_ - `Getting song lyrics by URL or ID`_ +- `Getting songs by tag (genre)`_ - `Getting the lyrics for all songs of a search`_ - `Searching for a song by lyrics`_ - `YouTube URL of artist's songs`_ @@ -101,8 +101,8 @@ Using :meth:`search_all `: for hit in request['sections'][2]['hits']: print(hit['result']['title']) -Getting songs that have a tag ------------------------------ +Getting songs by tag (genre) +---------------------------- Genius has the following main tags: ``rap``, ``pop``, ``r-b``, ``rock``, ``country``, ``non-music`` To discover more tags, visit the `Genius Tags`_ page. diff --git a/lyricsgenius/genius.py b/lyricsgenius/genius.py index f504d6fc..3beeaa1b 100644 --- a/lyricsgenius/genius.py +++ b/lyricsgenius/genius.py @@ -553,8 +553,7 @@ def find_artist_id(search_term): continue # Create the Song object from lyrics and metadata - if (song_info['lyrics_state'] == 'complete' - and not song_info.get('instrumental')): + if song_info['lyrics_state'] == 'complete': lyrics = self.lyrics(song_info['url']) else: lyrics = "" diff --git a/lyricsgenius/types/song.py b/lyricsgenius/types/song.py index 391b6d34..b61f7083 100644 --- a/lyricsgenius/types/song.py +++ b/lyricsgenius/types/song.py @@ -26,7 +26,6 @@ def __init__(self, client, json_dict, lyrics=""): self.full_title = body['full_title'] self.header_image_thumbnail_url = body['header_image_thumbnail_url'] self.header_image_url = body['header_image_url'] - self.instrumental = body.get('instrumental', False) self.lyrics_owner_id = body['lyrics_owner_id'] self.lyrics_state = body['lyrics_state'] self.path = body['path'] diff --git a/lyricsgenius/utils.py b/lyricsgenius/utils.py index ba306326..db05c2b5 100644 --- a/lyricsgenius/utils.py +++ b/lyricsgenius/utils.py @@ -72,7 +72,7 @@ def clean_str(s): """Cleans a string to help with string comparison. Removes punctuation and returns - a stripped, NFKD normalized string in lowercase. + a stripped, NFKC normalized string in lowercase. Args: s (:obj:`str`): A string. @@ -83,7 +83,7 @@ def clean_str(s): """ punctuation_ = punctuation + "’" string = s.translate(str.maketrans('', '', punctuation_)).strip().lower() - return unicodedata.normalize("NFKD", string) + return unicodedata.normalize("NFKC", string) def parse_redirected_url(url, flow): From 50e883d6c44dc781f83f4728219da7e780728067 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Wed, 11 Nov 2020 21:36:01 +0330 Subject: [PATCH 17/44] add some description for charts --- lyricsgenius/api/public_methods/leaderboard.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lyricsgenius/api/public_methods/leaderboard.py b/lyricsgenius/api/public_methods/leaderboard.py index 8c94672f..fd457cf2 100644 --- a/lyricsgenius/api/public_methods/leaderboard.py +++ b/lyricsgenius/api/public_methods/leaderboard.py @@ -11,7 +11,7 @@ def leaderboard(self, This method gets data of the community charts on the Genius.com page. Args: - time_period (:obj:`str`, optional): Time period of the results + time_period (:obj:`str`, optional): Time period of the results. ('day', 'week', 'month' or 'all_time'). per_page (:obj:`int`, optional): Number of results to return per request. It can't be more than 50. @@ -42,15 +42,19 @@ def charts(self, This method gets data of the chart on the Genius.com page. Args: - time_period (:obj:`str`, optional): Time period of the results + time_period (:obj:`str`, optional): Time period of the results. + The default is `all`. ('day', 'week', 'month' or 'all_time'). chart_genre (:obj:`str`, optional): The genre of the results. + The default value is ``all``. + ('all', 'rap', 'pop', 'rb', 'rock' or 'country') per_page (:obj:`int`, optional): Number of results to return per request. It can't be more than 50. page (:obj:`int`, optional): Paginated offset (number of the page). text_format (:obj:`str`, optional): Text format of the results ('dom', 'html', 'markdown' or 'plain'). type_ (:obj:`int`, optional): The type to get the charts for. + The default is ``songs``. ('songs', 'albums', 'artists' or 'referents'). Returns: @@ -59,7 +63,7 @@ def charts(self, .. Note:: The *referents* mentioned in the description of the :obj:`type_` argument is shown as *Lyrics* in the drop-down menu on Genius.com - where you can choose the *Type*. + where you choose the *Type*. """ endpoint = type_ + '/chart' From f2ab7162a844248fba303bfc65a68b70079fc628 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Wed, 11 Nov 2020 22:10:53 +0330 Subject: [PATCH 18/44] remove Song.instrumental references --- docs/src/reference/types.rst | 3 --- lyricsgenius/types/song.py | 1 - 2 files changed, 4 deletions(-) diff --git a/docs/src/reference/types.rst b/docs/src/reference/types.rst index 7be72269..75e16c88 100644 --- a/docs/src/reference/types.rst +++ b/docs/src/reference/types.rst @@ -192,9 +192,6 @@ Attributes * - id - :obj:`int` - * - instrumental - - :obj:`bool` - * - lyrics - :obj:`str` diff --git a/lyricsgenius/types/song.py b/lyricsgenius/types/song.py index b61f7083..bb0e5a25 100644 --- a/lyricsgenius/types/song.py +++ b/lyricsgenius/types/song.py @@ -40,7 +40,6 @@ def to_dict(self): body = super().to_dict() body['artist'] = self.artist body['lyrics'] = self.lyrics - body['instrumental'] = self.instrumental return body def to_json(self, From 1c393762c87dbc3b25b6477fff79e664b57a45b5 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Thu, 12 Nov 2020 20:58:36 +0330 Subject: [PATCH 19/44] added raising TypeError when no token/empty token is passed --- lyricsgenius/api/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lyricsgenius/api/base.py b/lyricsgenius/api/base.py index f58bd9be..a23699bb 100644 --- a/lyricsgenius/api/base.py +++ b/lyricsgenius/api/base.py @@ -27,7 +27,11 @@ def __init__( } if access_token is None: access_token = os.environ.get('GENIUS_ACCESS_TOKEN') - self.access_token = 'Bearer ' + access_token if access_token else None + + if not access_token or not isinstance(access_token, str): + raise TypeError('Invalid token') + + self.access_token = 'Bearer ' + access_token self.authorization_header = {'authorization': self.access_token} self.response_format = response_format.lower() self.timeout = timeout From bf8c0762b6d2e2960d3b7d9e7d75a2eadfff1669 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sat, 21 Nov 2020 23:03:21 +0330 Subject: [PATCH 20/44] - renamed album.songs to album.tracks - added Track type - docs: added 'release_data' to sort options in artist_songs - docs: added track docs --- docs/src/reference/types.rst | 10 ++++-- lyricsgenius/api/api.py | 2 +- lyricsgenius/genius.py | 23 +++++++------ lyricsgenius/types/album.py | 64 +++++++++++++++++++++++++++++++++--- tests/test_album.py | 6 ++-- 5 files changed, 85 insertions(+), 20 deletions(-) diff --git a/docs/src/reference/types.rst b/docs/src/reference/types.rst index 75e16c88..06ac0e2f 100644 --- a/docs/src/reference/types.rst +++ b/docs/src/reference/types.rst @@ -25,12 +25,18 @@ Classes :nosignatures: Stats + Track .. autoclass:: Stats :members: :member-order: bysource :no-show-inheritance: +.. autoclass:: Track + :members: + :member-order: bysource + :no-show-inheritance: + Album ------ @@ -74,8 +80,8 @@ Attributes * - release_date_components - :class:`datetime` - * - songs - - :obj:`list` + * - tracks + - :obj:`list` of :class:`Track` * - url - :obj:`str` diff --git a/lyricsgenius/api/api.py b/lyricsgenius/api/api.py index 26f15976..d5c16faa 100644 --- a/lyricsgenius/api/api.py +++ b/lyricsgenius/api/api.py @@ -312,7 +312,7 @@ def artist_songs(self, artist_id, per_page=None, page=None, sort='title'): Args: artist_id (:obj:`int`): Genius artist ID sort (:obj:`str`, optional): Sorting preference. - Either based on 'title' or 'popularity'. + Either based on 'title', 'popularity' or 'release_date'. per_page (:obj:`int`, optional): Number of results to return per request. It can't be more than 50. page (:obj:`int`, optional): Paginated offset (number of the page). diff --git a/lyricsgenius/genius.py b/lyricsgenius/genius.py index 3beeaa1b..c66b1e9a 100644 --- a/lyricsgenius/genius.py +++ b/lyricsgenius/genius.py @@ -13,7 +13,7 @@ from bs4 import BeautifulSoup from .api import API, PublicAPI -from .types import Album, Artist, Song +from .types import Album, Artist, Song, Track from .utils import clean_str, safe_unicode @@ -320,25 +320,28 @@ def search_album(self, name=None, artist="", album_id = album_info['id'] - songs = [] + tracks = [] next_page = 1 # It's unlikely for an album to have >=50 songs, # but it's best to check while next_page: - tracks = self.album_tracks(album_id=album_id, - per_page=50, - page=next_page, - text_format=text_format) - for track in tracks['tracks']: + tracks_list = self.album_tracks( + album_id=album_id, + per_page=50, + page=next_page, + text_format=text_format + ) + for track in tracks_list['tracks']: song_info = track['song'] if (song_info['lyrics_state'] == 'complete' and not song_info.get('instrumental')): song_lyrics = self.lyrics(song_info['url']) else: song_lyrics = "" - song = Song(self, song_info, song_lyrics) - songs.append(song) + + track = Track(self, track, song_lyrics) + tracks.append(track) next_page = tracks['next_page'] @@ -346,7 +349,7 @@ def search_album(self, name=None, artist="", new_info = self.album(album_id, text_format=text_format)['album'] album_info.update(new_info) - return Album(self, album_info, songs) + return Album(self, album_info, tracks) def search_song(self, title=None, artist="", song_id=None, get_full_info=True): diff --git a/lyricsgenius/types/album.py b/lyricsgenius/types/album.py index 3de47835..2e480336 100644 --- a/lyricsgenius/types/album.py +++ b/lyricsgenius/types/album.py @@ -1,18 +1,19 @@ from ..utils import convert_to_datetime from .base import BaseEntity from .artist import Artist +from .song import Song class Album(BaseEntity): """An album from the Genius.com database.""" - def __init__(self, client, json_dict, songs): + def __init__(self, client, json_dict, tracks): body = json_dict super().__init__(body['id']) self._body = body self._client = client self.artist = Artist(client, body['artist']) - self.songs = songs + self.tracks = tracks self.release_date_components = convert_to_datetime( body['release_date_components'] ) @@ -28,7 +29,7 @@ def __init__(self, client, json_dict, songs): def to_dict(self): body = super().to_dict() - body['songs'] = [song.to_dict() for song in self.songs] + body['tracks'] = [track.to_dict() for track in self.tracks] return body def to_json(self, @@ -45,7 +46,7 @@ def to_json(self, def to_text(self, filename=None, sanitize=True): - data = ' '.join(song.lyrics for song in self.songs) + data = ' '.join(track.song.lyrics for track in self.track) return super().to_text(data=data, filename=filename, @@ -67,3 +68,58 @@ def save_lyrics(self, ensure_ascii=ensure_ascii, sanitize=sanitize, verbose=verbose) + + +class Track(BaseEntity): + """docstring for Track""" + + def __init__(self, json_dict, lyrics): + body = json_dict + super().__init__(body['song']['id']) + self._body = body + + self.number = body['number'] + self.song = Song(body['song'], lyrics) + + def to_dict(self): + body = super().to_dict() + body['song'] = self.song.to_dict() + return body + + def to_json(self, + filename=None, + sanitize=True, + ensure_ascii=True): + data = self.to_dict() + + return super().to_json(data=data, + filename=filename, + sanitize=sanitize, + ensure_ascii=ensure_ascii) + + def to_text(self, + filename=None, + sanitize=True): + data = self.song.lyrics + + return super().to_text(data=data, + filename=filename, + sanitize=sanitize) + + def save_lyrics(self, + filename=None, + extension='json', + overwrite=False, + ensure_ascii=True, + sanitize=True, + verbose=True): + if filename is None: + filename = 'Lyrics_{:02d}_{}'.format(self.number, self.song.title) + filename = filename.replace(' ', '') + + return super().save_lyrics(filename=filename, + extension=extension, + overwrite=overwrite, + ensure_ascii=ensure_ascii, + sanitize=sanitize, + verbose=verbose) diff --git a/tests/test_album.py b/tests/test_album.py index 280d1ca3..03b97ed7 100644 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -18,7 +18,7 @@ def setUpClass(cls): warnings.simplefilter("ignore", ResourceWarning) cls.album_name = "The Party" cls.artist_name = "Andy Shauf" - cls.num_songs = 10 + cls.num_tracks = 10 cls.album = genius.search_album( cls.album_name, cls.artist_name @@ -33,8 +33,8 @@ def test_album_name(self): def test_album_artist(self): self.assertEqual(self.album.artist.name, self.artist_name) - def test_songs(self): - self.assertEqual(len(self.album.songs), self.num_songs) + def test_tracks(self): + self.assertEqual(len(self.album.tracks), self.num_tracks) def test_saving_json_file(self): print('\n') From 9b72d56b2252d3eb8e52e5d7323d4155d45a7088 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sat, 21 Nov 2020 23:09:11 +0330 Subject: [PATCH 21/44] added importing Track from types --- lyricsgenius/types/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyricsgenius/types/__init__.py b/lyricsgenius/types/__init__.py index 05dff346..752a3932 100644 --- a/lyricsgenius/types/__init__.py +++ b/lyricsgenius/types/__init__.py @@ -1,4 +1,4 @@ from .base import Stats -from .album import Album +from .album import Album, Track from .artist import Artist from .song import Song From db0c3b30ed193299aca30c48676884bac1f34bfe Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sat, 21 Nov 2020 23:31:28 +0330 Subject: [PATCH 22/44] - fixed issues in Track - added __repr__ for track --- lyricsgenius/genius.py | 2 +- lyricsgenius/types/album.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lyricsgenius/genius.py b/lyricsgenius/genius.py index c66b1e9a..7895254d 100644 --- a/lyricsgenius/genius.py +++ b/lyricsgenius/genius.py @@ -343,7 +343,7 @@ def search_album(self, name=None, artist="", track = Track(self, track, song_lyrics) tracks.append(track) - next_page = tracks['next_page'] + next_page = tracks_list['next_page'] if album_id is None and get_full_info is True: new_info = self.album(album_id, text_format=text_format)['album'] diff --git a/lyricsgenius/types/album.py b/lyricsgenius/types/album.py index 2e480336..e3ea3fdf 100644 --- a/lyricsgenius/types/album.py +++ b/lyricsgenius/types/album.py @@ -46,7 +46,7 @@ def to_json(self, def to_text(self, filename=None, sanitize=True): - data = ' '.join(track.song.lyrics for track in self.track) + data = ' '.join(track.song.lyrics for track in self.tracks) return super().to_text(data=data, filename=filename, @@ -73,13 +73,13 @@ def save_lyrics(self, class Track(BaseEntity): """docstring for Track""" - def __init__(self, json_dict, lyrics): + def __init__(self, client, json_dict, lyrics): body = json_dict super().__init__(body['song']['id']) self._body = body + self.song = Song(client, body['song'], lyrics) self.number = body['number'] - self.song = Song(body['song'], lyrics) def to_dict(self): body = super().to_dict() @@ -123,3 +123,7 @@ def save_lyrics(self, ensure_ascii=ensure_ascii, sanitize=sanitize, verbose=verbose) + + def __repr__(self): + name = self.__class__.__name__ + return "{}(number, song)".format(name) From b84221d13d3d62407ccfe4abc46340bedf0ad0a4 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sat, 28 Nov 2020 12:19:09 +0330 Subject: [PATCH 23/44] - reconfigured get_user_token to accept code and state parameters - updated snippets - added InvalidState for when self.state and passed state don't match --- docs/src/examples/snippets.rst | 11 +++++---- lyricsgenius/auth.py | 43 ++++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/docs/src/examples/snippets.rst b/docs/src/examples/snippets.rst index add987b7..35c345ba 100644 --- a/docs/src/examples/snippets.rst +++ b/docs/src/examples/snippets.rst @@ -170,7 +170,6 @@ URI will work (for example ``http://example.com/callback``) from lyricsgenius import OAuth2, Genius - # you can also use OAuth2.full_code_exchange() auth = OAuth2.client_only_app( 'my_client_id', 'my_redirect_uri', @@ -199,14 +198,18 @@ Authenticating another user 'my_client_id', 'my_redirect_uri', 'my_client_secret', - scope='all' + scope='all', + state='some_unique_value' ) # this part is the same url_for_user = auth.url print('Redirecting you to ' + url_for_user) - redirected_url = 'https://example.com/?code=some_code' - token = auth.get_user_token(redirected_url) + + # If we were using Flask: + code = request.args.get('code') + state = request.args.get('state') + token = auth.get_user_token(code, state) genius = Genius(token) diff --git a/lyricsgenius/auth.py b/lyricsgenius/auth.py index f93ccf8f..27e03b4b 100644 --- a/lyricsgenius/auth.py +++ b/lyricsgenius/auth.py @@ -67,11 +67,13 @@ def url(self): payload['state'] = self.state return OAuth2.auth_url + '?' + urlencode(payload) - def get_user_token(self, url, **kwargs): - """Gets a user token using the redirected URL. - This method will either get the value of the *token* - parameter in the redirected URL, or use the value of the + def get_user_token(self, code, state=None, **kwargs): + """Gets a user token using the code parameter. + This method will use the value of the *code* parameter to request a token from Genius. + If you provide a :obj:`state` it will also compare + it to initial state and will raise an exception if + they're not equal. Args: url (:obj:`str`): 'code' parameter of redirected URL. @@ -80,25 +82,26 @@ def get_user_token(self, url, **kwargs): :obj:`str`: User token. """ - if self.flow == 'code': - payload = {'code': parse_redirected_url(url, self.flow), - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'redirect_uri': self.redirect_uri, - 'grant_type': 'authorization_code', - 'response_type': 'code'} - url = OAuth2.token_url.replace('https://api.genius.com/', '') - res = self._make_request(url, 'POST', data=payload, **kwargs) - return res['access_token'] - elif self.flow == 'token': - return parse_redirected_url(url, self.flow) + + if state is not None and self.state != state: + raise InvalidState('States do not match.') + + payload = {'code': code, + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'redirect_uri': self.redirect_uri, + 'grant_type': 'authorization_code', + 'response_type': 'code'} + url = OAuth2.token_url.replace('https://api.genius.com/', '') + res = self._make_request(url, 'POST', data=payload, **kwargs) + return res['access_token'] def prompt_user(self): """Prompts current user for authentication. Opens a web browser for you to log in with Genius. Prompts to paste the URL after logging in to parse the - *code* or *token* URL parameter. + *token* URL parameter. returns: :obj:`str`: User token. @@ -110,7 +113,7 @@ def prompt_user(self): webbrowser.open(url) redirected = input('Please paste redirect URL: ').strip() - return self.get_user_token(redirected) + return parse_redirected_url(redirected, self.flow) @classmethod def client_only_app(cls, client_id, redirect_uri, scope=None, state=None): @@ -167,3 +170,7 @@ def __repr__(self): state=self.state, client_only_app=self.client_only_app ) + + +class InvalidState(Exception): + """Exception for non-matching states.""" From 2376a583d4f8368732a0716b9288a6de7bc4e9bd Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sat, 28 Nov 2020 23:02:48 +0330 Subject: [PATCH 24/44] - updated get_user_token docstring - moved InvalidState exception to errors.py --- lyricsgenius/auth.py | 9 ++++----- lyricsgenius/errors.py | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 lyricsgenius/errors.py diff --git a/lyricsgenius/auth.py b/lyricsgenius/auth.py index 27e03b4b..180dfead 100644 --- a/lyricsgenius/auth.py +++ b/lyricsgenius/auth.py @@ -3,6 +3,7 @@ from .utils import parse_redirected_url from .api import Sender +from .errors import InvalidState class OAuth2(Sender): @@ -76,7 +77,9 @@ def get_user_token(self, code, state=None, **kwargs): they're not equal. Args: - url (:obj:`str`): 'code' parameter of redirected URL. + code (:obj:`str`): 'code' parameter of redirected URL. + state (:obj:`str`): state parameter of redirected URL (only + provide if you want to compare with initial :obj:`self.state`) **kwargs: keywords for the POST request. returns: :obj:`str`: User token. @@ -170,7 +173,3 @@ def __repr__(self): state=self.state, client_only_app=self.client_only_app ) - - -class InvalidState(Exception): - """Exception for non-matching states.""" diff --git a/lyricsgenius/errors.py b/lyricsgenius/errors.py new file mode 100644 index 00000000..e3c78e02 --- /dev/null +++ b/lyricsgenius/errors.py @@ -0,0 +1,2 @@ +class InvalidState(Exception): + """Exception for non-matching states.""" From 4573de52aab7551c3c47d8c18f9a5d90d6946ae3 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 29 Nov 2020 00:23:39 +0330 Subject: [PATCH 25/44] - removed test_get_user_token_client_flow as its unnecessary - fixed prompt_user to allow the code flow --- lyricsgenius/auth.py | 8 +++++++- tests/test_auth.py | 20 +++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lyricsgenius/auth.py b/lyricsgenius/auth.py index 180dfead..561cf3bc 100644 --- a/lyricsgenius/auth.py +++ b/lyricsgenius/auth.py @@ -116,7 +116,13 @@ def prompt_user(self): webbrowser.open(url) redirected = input('Please paste redirect URL: ').strip() - return parse_redirected_url(redirected, self.flow) + if self.flow == 'token': + token = parse_redirected_url(redirected, self.flow) + else: + code = parse_redirected_url(redirected, self.flow) + token = self.get_user_token(code) + + return token @classmethod def client_only_app(cls, client_id, redirect_uri, scope=None, state=None): diff --git a/tests/test_auth.py b/tests/test_auth.py index 2bac4573..f24951ea 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -60,25 +60,19 @@ def test_init(self): client_secret, scope='all') self.assertEqual(auth.scope, scope) - def test_get_user_token_client_flow(self): - # client-only flow - auth = OAuth2(client_id, redirect_uri, client_only_app=True) - redirected = 'https://example.com/callback#access_token=test' - client_flow_token = 'test' - - r = auth.get_user_token(redirected) - self.assertEqual(r, client_flow_token) - @patch('requests.Session.request', side_effect=mocked_requests_post) def test_get_user_token_code_flow(self, mock_post): # full code exchange flow - auth = OAuth2(client_id, redirect_uri, - client_secret, scope='all') - redirected = 'https://example.com/callback?code=some_code' + + state = 'some_state' + code = 'some_code' code_flow_token = 'test' - r = auth.get_user_token(redirected) + auth = OAuth2(client_id, redirect_uri, + client_secret, scope='all', state=state) + + r = auth.get_user_token(code, state) self.assertEqual(r, code_flow_token) def test_prompt_user(self): From 65b29806d9ff4760cd2e2e376bfb7242db509960 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Mon, 7 Dec 2020 18:19:10 +0330 Subject: [PATCH 26/44] - refactored get_user_token to allow use by token flow - added some tests for auth --- lyricsgenius/auth.py | 47 +++++++++++++++++++++-------------- lyricsgenius/errors.py | 2 +- tests/test_auth.py | 56 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/lyricsgenius/auth.py b/lyricsgenius/auth.py index 561cf3bc..5f98785e 100644 --- a/lyricsgenius/auth.py +++ b/lyricsgenius/auth.py @@ -3,7 +3,7 @@ from .utils import parse_redirected_url from .api import Sender -from .errors import InvalidState +from .errors import InvalidStateError class OAuth2(Sender): @@ -68,16 +68,22 @@ def url(self): payload['state'] = self.state return OAuth2.auth_url + '?' + urlencode(payload) - def get_user_token(self, code, state=None, **kwargs): - """Gets a user token using the code parameter. - This method will use the value of the - *code* parameter to request a token from Genius. - If you provide a :obj:`state` it will also compare - it to initial state and will raise an exception if + def get_user_token(self, code=None, url=None, state=None, **kwargs): + """Gets a user token using the url or the code parameter.. + If you supply value for :obj:`code`, this method will use the value of the + :obj:`code` parameter to request a token from Genius. + + If you use the :method`client_only_app` and supplt the redirected URL, + it will already have the token. + You could pass the URL to this method or parse it yourself. + + If you provide a :obj:`state` the method will also compare + it to the initial state and will raise an exception if they're not equal. Args: code (:obj:`str`): 'code' parameter of redirected URL. + url (:obj:`str`): Redirected URL (used in client-only apps) state (:obj:`str`): state parameter of redirected URL (only provide if you want to compare with initial :obj:`self.state`) **kwargs: keywords for the POST request. @@ -85,19 +91,24 @@ def get_user_token(self, code, state=None, **kwargs): :obj:`str`: User token. """ + assert any(code, url), "You must pass either `code` or `url`." if state is not None and self.state != state: - raise InvalidState('States do not match.') - - payload = {'code': code, - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'redirect_uri': self.redirect_uri, - 'grant_type': 'authorization_code', - 'response_type': 'code'} - url = OAuth2.token_url.replace('https://api.genius.com/', '') - res = self._make_request(url, 'POST', data=payload, **kwargs) - return res['access_token'] + raise InvalidStateError('States do not match.') + + if code: + payload = {'code': code, + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'redirect_uri': self.redirect_uri, + 'grant_type': 'authorization_code', + 'response_type': 'code'} + url = OAuth2.token_url.replace('https://api.genius.com/', '') + res = self._make_request(url, 'POST', data=payload, **kwargs) + token = res['access_token'] + else: + token = parse_redirected_url(url, self.flow) + return token def prompt_user(self): """Prompts current user for authentication. diff --git a/lyricsgenius/errors.py b/lyricsgenius/errors.py index e3c78e02..911a7002 100644 --- a/lyricsgenius/errors.py +++ b/lyricsgenius/errors.py @@ -1,2 +1,2 @@ -class InvalidState(Exception): +class InvalidStateError(Exception): """Exception for non-matching states.""" diff --git a/tests/test_auth.py b/tests/test_auth.py index f24951ea..9f710b40 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -4,6 +4,7 @@ from lyricsgenius import OAuth2 +from lyricsgenius.errors import InvalidStateError client_id = os.environ["GENIUS_CLIENT_ID"] client_secret = os.environ["GENIUS_CLIENT_SECRET"] @@ -69,12 +70,61 @@ def test_get_user_token_code_flow(self, mock_post): code = 'some_code' code_flow_token = 'test' - auth = OAuth2(client_id, redirect_uri, - client_secret, scope='all', state=state) + auth = OAuth2.full_code_exchange( + client_id, + redirect_uri, + client_secret, + scope='all', + state=state + ) - r = auth.get_user_token(code, state) + r = auth.get_user_token(code=code, state=state) self.assertEqual(r, code_flow_token) + def test_get_user_token_token_flow(self): + + state = 'some_state' + token_flow_token = 'test' + redirected_url = '{}#access_token=test'.format(redirect_uri) + + auth = OAuth2.client_only_app( + client_id, + redirect_uri, + scope='all', + state=state + ) + + r = auth.get_user_token(url=redirected_url) + self.assertEqual(r, token_flow_token) + + def test_get_user_token_invalid_state(self): + state = 'state_1' + auth = OAuth2.full_code_exchange( + client_id, + redirect_uri, + client_secret, + scope='all', + state=state + ) + + returned_code = 'some_code' + returned_state = 'state_2' + with self.assertRaises(InvalidStateError): + auth.get_user_token(code=returned_code, state=returned_state) + + def test_get_user_token_no_parameter(self): + state = 'some_state' + auth = OAuth2.full_code_exchange( + client_id, + redirect_uri, + client_secret, + scope='all', + state=state + ) + + with self.assertRaises(AssertionError): + auth.get_user_token() + def test_prompt_user(self): auth = OAuth2(client_id, redirect_uri, client_secret, scope='all') From 402eaf5ea2d78cf6b13f59c74a04bf631fb84d09 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Mon, 7 Dec 2020 18:20:25 +0330 Subject: [PATCH 27/44] removed redundant requirements.txt file --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d915612c..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -beautifulsoup4>=4.6.0 -requests>=2.20.0 From 609b1bb0a26fefd515a0ec1f202e8eefeda05527 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Mon, 7 Dec 2020 18:52:59 +0330 Subject: [PATCH 28/44] fixed tests issue --- lyricsgenius/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyricsgenius/auth.py b/lyricsgenius/auth.py index 5f98785e..594d5eb1 100644 --- a/lyricsgenius/auth.py +++ b/lyricsgenius/auth.py @@ -91,7 +91,7 @@ def get_user_token(self, code=None, url=None, state=None, **kwargs): :obj:`str`: User token. """ - assert any(code, url), "You must pass either `code` or `url`." + assert any([code, url]), "You must pass either `code` or `url`." if state is not None and self.state != state: raise InvalidStateError('States do not match.') From bcf14f71351d83bd71f0c5adf9f5eec2d23ba98c Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Wed, 9 Dec 2020 21:52:36 +0330 Subject: [PATCH 29/44] - added page_data endpoint to PublicAPI - added newline after docstring where missing --- lyricsgenius/api/public_methods/article.py | 1 + lyricsgenius/api/public_methods/cover_art.py | 1 + lyricsgenius/api/public_methods/misc.py | 82 ++++++++++++++++++++ lyricsgenius/api/public_methods/question.py | 1 + lyricsgenius/api/public_methods/referent.py | 1 + lyricsgenius/api/public_methods/search.py | 1 + lyricsgenius/auth.py | 2 + 7 files changed, 89 insertions(+) diff --git a/lyricsgenius/api/public_methods/article.py b/lyricsgenius/api/public_methods/article.py index 7d007c5d..b4224da2 100644 --- a/lyricsgenius/api/public_methods/article.py +++ b/lyricsgenius/api/public_methods/article.py @@ -40,6 +40,7 @@ def article_comments(self, article_id, per_page=None, page=None, text_format=Non def latest_articles(self, per_page=None, page=None, text_format=None): """Gets the latest articles on the homepage. + This method will return the featured articles that are placed on top of the Genius.com page. diff --git a/lyricsgenius/api/public_methods/cover_art.py b/lyricsgenius/api/public_methods/cover_art.py index 6425c671..2d1d8a8d 100644 --- a/lyricsgenius/api/public_methods/cover_art.py +++ b/lyricsgenius/api/public_methods/cover_art.py @@ -3,6 +3,7 @@ class CoverArtMethods(object): def cover_arts(self, album_id=None, song_id=None, text_format=None): """Gets the cover arts of an album or a song. + You must supply one of :obj:`album_id` or :obj:`song_id`. Args: diff --git a/lyricsgenius/api/public_methods/misc.py b/lyricsgenius/api/public_methods/misc.py index d72b6ffe..bcdba355 100644 --- a/lyricsgenius/api/public_methods/misc.py +++ b/lyricsgenius/api/public_methods/misc.py @@ -23,12 +23,94 @@ def line_item(self, line_item_id, text_format=None): params = {'text_format': text_format or self.response_format} return self._make_request(path=endpoint, params_=params, public_api=True) + def page_data(self, album=None, song=None, artist=None): + """Gets page data of an item. + + If you want the page data of a song, you must supply + song and artist. But if you want the page data of an album, + you only have to supply the album. + + Page data will return all possible values for the album/song and + the lyrics in HTML format if the item is a song! + Album page data will contian album info and tracks info as well. + + Args: + album (:obj:`str`, optional): Album path + (e.g. '/albums/Eminem/Music-to-be-murdered-by') + song (:obj:`str`, optional): Song path + (e.g. '/Sia-chandelier-lyrics') + artist (:obj:`str`, optional): Artist slug. (e.g. 'Andy-shauf') + + Returns: + :obj:`dict` + + Warning: + Some albums/songs either don't have page data or + their page data path can't be infered easily from + the artist slug and their API path. So make sure to + use this method with a try/except clause that catches + 404 errors. Check out the example below. + + + Examples: + Getting the lyrics of a song from its page data + + .. code:: python + + from lyricsgenius import Genius, PublicAPI + from bs4 import BeautifulSoup + from requests import HTTPError + + genius = Genius(token) + public = PublicAPI() + + # We need the PublicAPI to get artist's slug + artist = public.artist(1655) + artist_slug = artist['artist']['slug'] + + # The rest can be done using Genius + song = genius.song(4558484) + song_path = song['song']['path'] + + try: + page_data = genius.page_data(artist=artist_slug, song=song_path) + except HTTPError as e: + print("Couldn't find page data {}".format(e.status_code)) + page_data = None + + if page_data is not None: + lyrics_html = page_data['page_data']['lyrics_data']['body']['html'] + lyrics_text = BeautifulSoup(lyrics_html, 'html.parser').get_text() + + """ + assert any([album, song]), "You must pass either song or album." + if song: + assert all([song, artist]), "You must pass artist." + + if album: + endpoint = 'page_data/album' + page_type = 'albums' + item_path = album.replace('/albums/', '') + else: + endpoint = 'page_data/song' + page_type = 'songs' + + # item path becomes something like: Artist/Song + item_path = song.replace(artist, artist + '/').replace('-lyrics', '') + + page_path = '/{page_type}/{item_path}'.format(page_type=page_type, + item_path=item_path) + params = {'page_path': page_path} + + return self._make_request(endpoint, params_=params, public_api=True) + def voters(self, annotation_id=None, answer_id=None, article_id=None, comment_id=None): """Gets the voters of an item. + You must supply one of :obj:`annotation_id`, :obj:`answer_id`, :obj:`article_id` or :obj:`comment_id`. diff --git a/lyricsgenius/api/public_methods/question.py b/lyricsgenius/api/public_methods/question.py index f1ed0022..aab7d22f 100644 --- a/lyricsgenius/api/public_methods/question.py +++ b/lyricsgenius/api/public_methods/question.py @@ -9,6 +9,7 @@ def questions(self, state=None, text_format=None): """Gets the questions on an album or a song. + You must supply one of :obj:`album_id` or :obj:`song_id`. Args: diff --git a/lyricsgenius/api/public_methods/referent.py b/lyricsgenius/api/public_methods/referent.py index c2fd60ed..75bcfece 100644 --- a/lyricsgenius/api/public_methods/referent.py +++ b/lyricsgenius/api/public_methods/referent.py @@ -3,6 +3,7 @@ class ReferentMethods(object): def referent(self, referent_ids, text_format=None): """Gets data of one or more referents. + This method can get multiple referents in one call, thus increasing performance. diff --git a/lyricsgenius/api/public_methods/search.py b/lyricsgenius/api/public_methods/search.py index 3562d7bc..d772804c 100644 --- a/lyricsgenius/api/public_methods/search.py +++ b/lyricsgenius/api/public_methods/search.py @@ -186,6 +186,7 @@ def search_videos(self, search_term, per_page=None, page=None): def search_all(self, search_term, per_page=None, page=None): """Searches all types. + Including: albums, articles, lyrics, songs, users and videos. diff --git a/lyricsgenius/auth.py b/lyricsgenius/auth.py index 594d5eb1..165c610e 100644 --- a/lyricsgenius/auth.py +++ b/lyricsgenius/auth.py @@ -51,6 +51,7 @@ def __init__(self, client_id, redirect_uri, @property def url(self): """Returns the URL you redirect the user to. + You can use this property to get a URL that when opened on the user's device, shows Genius's authorization page where user clicks *Agree* to give your app access, and then Genius redirects user back to your @@ -70,6 +71,7 @@ def url(self): def get_user_token(self, code=None, url=None, state=None, **kwargs): """Gets a user token using the url or the code parameter.. + If you supply value for :obj:`code`, this method will use the value of the :obj:`code` parameter to request a token from Genius. From df213e3657e3c92c360fd98810744953fc83a945 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 17 Jan 2021 23:00:19 +0330 Subject: [PATCH 30/44] handle instance where error response might not be json --- lyricsgenius/api/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lyricsgenius/api/base.py b/lyricsgenius/api/base.py index a23699bb..b1bc51f9 100644 --- a/lyricsgenius/api/base.py +++ b/lyricsgenius/api/base.py @@ -1,5 +1,6 @@ import time import os +from json.decoder import JSONDecodeError import requests from requests.exceptions import HTTPError, Timeout @@ -99,7 +100,10 @@ def _make_request( def get_description(e): error = str(e) - res = e.response.json() + try: + res = e.response.json() + except JSONDecodeError: + res = {} description = (res['meta']['message'] if res.get('meta') else res.get('error_description')) From c0ad3eb1a953979481528a5bd20fd53f6289a69a Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Tue, 19 Jan 2021 23:45:42 +0330 Subject: [PATCH 31/44] added test for page_data --- tests/test_public_methods.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_public_methods.py b/tests/test_public_methods.py index dcb96af7..eb7e3e40 100644 --- a/tests/test_public_methods.py +++ b/tests/test_public_methods.py @@ -5,7 +5,7 @@ from lyricsgenius import PublicAPI -from . import test_vcr +from . import test_vcr, genius client = PublicAPI() @@ -445,6 +445,24 @@ def setUpClass(cls): # r = client.line_item(self.line_item_id) # self.assertTrue("line_item" in r) + @test_vcr.use_cassette + def test_page_data_album(self): + album_path = '/albums/Eminem/Music-to-be-murdered-by' + + page_data = genius.page_data(album=album_path) + self.assertTrue('page_data' in page_data) + + @test_vcr.use_cassette + def test_page_data_song(self): + artist = client.artist(1655) + artist_slug = artist['artist']['slug'] + + song = genius.song(4558484) + song_path = song['song']['path'] + + page_data = genius.page_data(artist=artist_slug, song=song_path) + self.assertTrue('page_data' in page_data) + @test_vcr.use_cassette def test_voters(self): r = client.voters(annotation_id=self.annotation_id) From 0385abd436062335263b8a18c08af2bded8798b5 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Wed, 20 Jan 2021 00:28:56 +0330 Subject: [PATCH 32/44] fix typo in artist ID in example and test --- lyricsgenius/api/public_methods/misc.py | 2 +- tests/test_public_methods.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lyricsgenius/api/public_methods/misc.py b/lyricsgenius/api/public_methods/misc.py index bcdba355..cfc6e873 100644 --- a/lyricsgenius/api/public_methods/misc.py +++ b/lyricsgenius/api/public_methods/misc.py @@ -65,7 +65,7 @@ def page_data(self, album=None, song=None, artist=None): public = PublicAPI() # We need the PublicAPI to get artist's slug - artist = public.artist(1655) + artist = public.artist(1665) artist_slug = artist['artist']['slug'] # The rest can be done using Genius diff --git a/tests/test_public_methods.py b/tests/test_public_methods.py index eb7e3e40..a76f2776 100644 --- a/tests/test_public_methods.py +++ b/tests/test_public_methods.py @@ -454,7 +454,7 @@ def test_page_data_album(self): @test_vcr.use_cassette def test_page_data_song(self): - artist = client.artist(1655) + artist = client.artist(1665) artist_slug = artist['artist']['slug'] song = genius.song(4558484) From 8cf520d04985f294703dcb7c9b03f3a1ee935c43 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Wed, 20 Jan 2021 00:56:51 +0330 Subject: [PATCH 33/44] fixed TypeError when instantiating PublicAPI directly with no token present in env vars fixed page_data for song which returned 404 --- lyricsgenius/api/api.py | 6 ++++++ lyricsgenius/api/base.py | 14 +++++++++----- lyricsgenius/api/public_methods/misc.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lyricsgenius/api/api.py b/lyricsgenius/api/api.py index d5c16faa..b7f3a0ba 100644 --- a/lyricsgenius/api/api.py +++ b/lyricsgenius/api/api.py @@ -526,11 +526,17 @@ def __init__( retries=0, **kwargs ): + + # If PublicAPI was instantiated directly + # there is no need for a token anymore + public_api_constructor = False if 'Genius' in str(self) else True + # Genius PublicAPI Constructor super().__init__( response_format=response_format, timeout=timeout, sleep_time=sleep_time, retries=retries, + public_api_constructor=public_api_constructor, **kwargs ) diff --git a/lyricsgenius/api/base.py b/lyricsgenius/api/base.py index b1bc51f9..31d73122 100644 --- a/lyricsgenius/api/base.py +++ b/lyricsgenius/api/base.py @@ -19,7 +19,8 @@ def __init__( response_format='plain', timeout=5, sleep_time=0.2, - retries=0 + retries=0, + public_api_constructor=False, ): self._session = requests.Session() self._session.headers = { @@ -29,11 +30,14 @@ def __init__( if access_token is None: access_token = os.environ.get('GENIUS_ACCESS_TOKEN') - if not access_token or not isinstance(access_token, str): - raise TypeError('Invalid token') + if public_api_constructor: + self.authorization_header = {} + else: + if not access_token or not isinstance(access_token, str): + raise TypeError('Invalid token') + self.access_token = 'Bearer ' + access_token + self.authorization_header = {'authorization': self.access_token} - self.access_token = 'Bearer ' + access_token - self.authorization_header = {'authorization': self.access_token} self.response_format = response_format.lower() self.timeout = timeout self.sleep_time = sleep_time diff --git a/lyricsgenius/api/public_methods/misc.py b/lyricsgenius/api/public_methods/misc.py index cfc6e873..acd56d88 100644 --- a/lyricsgenius/api/public_methods/misc.py +++ b/lyricsgenius/api/public_methods/misc.py @@ -96,7 +96,7 @@ def page_data(self, album=None, song=None, artist=None): page_type = 'songs' # item path becomes something like: Artist/Song - item_path = song.replace(artist, artist + '/').replace('-lyrics', '') + item_path = song[1:].replace(artist + '-', artist + '/').replace('-lyrics', '') page_path = '/{page_type}/{item_path}'.format(page_type=page_type, item_path=item_path) From 7cc60e65f93361576ca0cec00fe57269d31902a7 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Wed, 20 Jan 2021 01:07:55 +0330 Subject: [PATCH 34/44] use built-in class name to define public api constructor --- lyricsgenius/api/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyricsgenius/api/api.py b/lyricsgenius/api/api.py index b7f3a0ba..3827c62b 100644 --- a/lyricsgenius/api/api.py +++ b/lyricsgenius/api/api.py @@ -529,7 +529,7 @@ def __init__( # If PublicAPI was instantiated directly # there is no need for a token anymore - public_api_constructor = False if 'Genius' in str(self) else True + public_api_constructor = False if self.__class__.__name__ == 'Genius' else True # Genius PublicAPI Constructor super().__init__( From 838cc21d7c1394b33a24ce2b129b0d4701f481a9 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Wed, 27 Jan 2021 17:08:02 +0330 Subject: [PATCH 35/44] genius.lyrics: broke down urlthing parameter into song_url and song_id --- docs/src/examples/snippets.rst | 6 +++--- lyricsgenius/genius.py | 24 ++++++++++++++---------- tests/test_genius.py | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/src/examples/snippets.rst b/docs/src/examples/snippets.rst index 35c345ba..256f4953 100644 --- a/docs/src/examples/snippets.rst +++ b/docs/src/examples/snippets.rst @@ -23,7 +23,7 @@ Getting song lyrics by URL or ID # Using Song URL url = "https://genius.com/Andy-shauf-begin-again-lyrics" - genius.lyrics(url) + genius.lyrics(song_url=url) # Using Song ID # Requires an extra request to get song URL @@ -121,7 +121,7 @@ Genius probably has more than 1000 songs with the pop tag. while page: res = genius.tag('pop', page=page) for hit in res['hits']: - song_lyrics = genius.lyrics(hit['url']) + song_lyrics = genius.lyrics(song_url=hit['url']) lyrics.append(song_lyrics) page = res['next_page'] @@ -135,7 +135,7 @@ Getting the lyrics for all songs of a search songs = genius.search_songs('Begin Again Andy Shauf') for song in songs['hits']: url = song['result']['url'] - song_lyrics = genius.lyrics(url) + song_lyrics = genius.lyrics(song_url=url) # id = song['result']['id'] # song_lyrics = genius.lyrics(id) lyrics.append(song_lyrics) diff --git a/lyricsgenius/genius.py b/lyricsgenius/genius.py index 7895254d..e9368f5a 100644 --- a/lyricsgenius/genius.py +++ b/lyricsgenius/genius.py @@ -92,12 +92,14 @@ def __init__(self, access_token=None, self.excluded_terms = self.default_terms.copy() self.excluded_terms.extend(excluded_terms) - def lyrics(self, urlthing, remove_section_headers=False): + def lyrics(self, song_id=None, song_url=None, remove_section_headers=False): """Uses BeautifulSoup to scrape song info off of a Genius song URL + You must supply either `song_id` or song_url`. + Args: - urlthing (:obj:`str` | :obj:`int`): - Song ID or song URL. + song_id (:obj:`int`, optional): Song ID. + song_url (:obj:`str`, optional): Song URL. remove_section_headers (:obj:`bool`, optional): If `True`, removes [Chorus], [Bridge], etc. headers from lyrics. @@ -118,10 +120,12 @@ def lyrics(self, urlthing, remove_section_headers=False): :attr:`Genius.remove_section_headers` attribute. """ - if isinstance(urlthing, int): - path = self.song(urlthing)['song']['path'][1:] + msg = "You must supply either `song_id` or `song_url`." + assert any([song_id, song_url]), msg + if song_url: + path = song_url.replace("https://genius.com/", "") else: - path = urlthing.replace("https://genius.com/", "") + path = self.song(song_id)['song']['path'][1:] # Scrape the song lyrics from the HTML html = BeautifulSoup( @@ -336,7 +340,7 @@ def search_album(self, name=None, artist="", song_info = track['song'] if (song_info['lyrics_state'] == 'complete' and not song_info.get('instrumental')): - song_lyrics = self.lyrics(song_info['url']) + song_lyrics = self.lyrics(song_url=song_info['url']) else: song_lyrics = "" @@ -429,7 +433,7 @@ def search_song(self, title=None, artist="", song_id=None, if (song_info['lyrics_state'] == 'complete' and not song_info.get('instrumental')): - lyrics = self.lyrics(song_info['url']) + lyrics = self.lyrics(song_url=song_info['url']) else: lyrics = "" @@ -557,7 +561,7 @@ def find_artist_id(search_term): # Create the Song object from lyrics and metadata if song_info['lyrics_state'] == 'complete': - lyrics = self.lyrics(song_info['url']) + lyrics = self.lyrics(song_url=song_info['url']) else: lyrics = "" if get_full_info: @@ -683,7 +687,7 @@ def tag(self, name, page=None): while page: res = genius.tag('pop', page=page) for hit in res['hits']: - song_lyrics = genius.lyrics(hit['url']) + song_lyrics = genius.lyrics(song_url=hit['url']) lyrics.append(song_lyrics) page = res['next_page'] diff --git a/tests/test_genius.py b/tests/test_genius.py index 1949f63c..d1c3e9ef 100644 --- a/tests/test_genius.py +++ b/tests/test_genius.py @@ -99,7 +99,7 @@ def setUpClass(cls): @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), serializer='yaml') def test_lyrics_with_url(self): - lyrics = genius.lyrics(self.song_url) + lyrics = genius.lyrics(song_url=self.song_url) self.assertTrue(lyrics.endswith(self.lyrics_ending)) @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), From c51cb8b5905d2d94c777a35d6903ce6740ea0a05 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Thu, 28 Jan 2021 17:39:24 +0330 Subject: [PATCH 36/44] added non-breaking space to punctuation words (genius adds this character before strings that their first letter is lower case) --- lyricsgenius/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyricsgenius/utils.py b/lyricsgenius/utils.py index db05c2b5..a29995fe 100644 --- a/lyricsgenius/utils.py +++ b/lyricsgenius/utils.py @@ -81,7 +81,7 @@ def clean_str(s): :obj:`str`: Cleaned string. """ - punctuation_ = punctuation + "’" + punctuation_ = punctuation + "’" + "\u200b" string = s.translate(str.maketrans('', '', punctuation_)).strip().lower() return unicodedata.normalize("NFKC", string) From 08691cae24f7816779a55148e3cbea65fc8296e3 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sat, 6 Feb 2021 23:59:11 +0330 Subject: [PATCH 37/44] linked to the snippets page in the usage page --- docs/src/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/usage.rst b/docs/src/usage.rst index 8a762a1e..73c4c057 100644 --- a/docs/src/usage.rst +++ b/docs/src/usage.rst @@ -87,7 +87,7 @@ Search for five songs by ‘The Beatles’ and save the lyrics: python3 -m lyricsgenius artist "The Beatles" --max-songs 5 --save -There also examples under the docs of some methods. +You might also like checking out the :ref:`snippets` page. .. toctree:: From 0cf0b3d47c7f58b8821380641b48086eab3189b0 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 7 Feb 2021 20:58:40 +0330 Subject: [PATCH 38/44] Revert "add vcrpy to requirements" This reverts commit 92816fd5c4d315ceba8912c1c48f84c827642e52. --- setup.py | 5 +---- tox.ini | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index c4869c96..202928d3 100644 --- a/setup.py +++ b/setup.py @@ -35,13 +35,10 @@ 'flake8', 'flake8-bugbear', 'pygments', - ], - 'tests': [ - 'vcrpy~=4.1' ] } extras_require['dev'] = ( - extras_require['docs'] + extras_require['checks'] + extras_require['tests'] + extras_require['docs'] + extras_require['checks'] ) setup( diff --git a/tox.ini b/tox.ini index a0c750f4..a783badf 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,6 @@ passenv = GENIUS* [testenv:test] ; Inherit everything from testenv -extras = tests [testenv:docs] description = Build Sphinx HTML documentation From 468a8060b38a59eb0e9eef698dce673812b3f60d Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 7 Feb 2021 21:02:45 +0330 Subject: [PATCH 39/44] Revert adding vcrpy to tests --- tests/__init__.py | 15 --------- tests/test_album.py | 6 +--- tests/test_api.py | 7 +--- tests/test_artist.py | 17 +--------- tests/test_genius.py | 11 +------ tests/test_public_methods.py | 64 +----------------------------------- tests/test_song.py | 7 +--- 7 files changed, 6 insertions(+), 121 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index a0138210..4567ee58 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,7 +1,5 @@ import os -import vcr - from lyricsgenius import Genius @@ -12,16 +10,3 @@ # Genius client genius = Genius(access_token, sleep_time=1.0, timeout=15, retries=3) - -cassettes_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - 'fixtures/cassettes' -) - -# VCR Configs -test_vcr = vcr.VCR( - path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml', - cassette_library_dir=cassettes_path, - filter_headers=['authorization'] -) diff --git a/tests/test_album.py b/tests/test_album.py index 03b97ed7..0e9c872e 100644 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -2,17 +2,13 @@ import os import warnings -import vcr - -from . import genius, test_vcr +from . import genius from lyricsgenius.types import Album class TestAlbum(unittest.TestCase): @classmethod - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix(' album.yaml'), - serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Album tests...\n") warnings.simplefilter("ignore", ResourceWarning) diff --git a/tests/test_api.py b/tests/test_api.py index 9711bd66..1b173d9f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,7 @@ import unittest -from . import genius, test_vcr +from . import genius class TestAPI(unittest.TestCase): @@ -10,14 +10,12 @@ class TestAPI(unittest.TestCase): def setUpClass(cls): print("\n---------------------\nSetting up API tests...\n") - @test_vcr.use_cassette def test_account(self): msg = ("No user detail was returned. " "Are you sure you're using a user access token?") r = genius.account() self.assertTrue("user" in r, msg) - @test_vcr.use_cassette def test_annotation(self): msg = "Returned annotation API path is different than expected." id_ = 10225840 @@ -26,7 +24,6 @@ def test_annotation(self): expected = '/annotations/10225840' self.assertEqual(real, expected, msg) - @test_vcr.use_cassette def test_manage_annotation(self): example_text = 'The annotation' new_annotation = genius.create_annotation( @@ -65,7 +62,6 @@ def test_manage_annotation(self): r = genius.delete_annotation(new_annotation['id']) self.assertEqual(r, 204, msg) - @test_vcr.use_cassette def test_referents_web_page(self): msg = "Returned referent API path is different than expected." id_ = 10347 @@ -84,7 +80,6 @@ def test_referents_invalid_input(self): with self.assertRaises(AssertionError): genius.referents(song_id=1, web_page_id=1) - @test_vcr.use_cassette def test_web_page(self): msg = "Returned web page API path is different than expected." url = "https://docs.genius.com" diff --git a/tests/test_artist.py b/tests/test_artist.py index a830e4f5..42c852b3 100644 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -1,10 +1,7 @@ import unittest import os - -import vcr - -from . import genius, test_vcr +from . import genius from lyricsgenius.types import Artist from lyricsgenius.utils import sanitize_filename @@ -12,8 +9,6 @@ class TestArtist(unittest.TestCase): @classmethod - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix(' artist.yaml'), - serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Artist tests...\n") @@ -27,15 +22,12 @@ def test_artist(self): msg = "The returned object is not an instance of the Artist class." self.assertIsInstance(self.artist, Artist, msg) - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml') def test_correct_artist_name(self): msg = "Returned artist name does not match searched artist." name = "Queen" result = genius.search_artist(name, max_songs=1).name self.assertEqual(name, result, msg) - @test_vcr.use_cassette def test_zero_songs(self): msg = "Songs were downloaded even though 0 songs was requested." name = "Queen" @@ -46,28 +38,21 @@ def test_name(self): msg = "The artist object name does not match the requested artist name." self.assertEqual(self.artist.name, self.artist_name, msg) - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml') def test_add_song_from_same_artist(self): msg = "The new song was not added to the artist object." self.artist.add_song(genius.search_song(self.new_song, self.artist_name)) self.assertEqual(self.artist.num_songs, self.max_songs + 1, msg) - @test_vcr.use_cassette def test_song(self): msg = "Song was not in artist's songs." song = self.artist.song(self.new_song) self.assertIsNotNone(song, msg) - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml') def test_add_song_from_different_artist(self): msg = "A song from a different artist was incorrectly allowed to be added." self.artist.add_song(genius.search_song("These Days", "Jackson Browne")) self.assertEqual(self.artist.num_songs, self.max_songs, msg) - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml') def test_artist_with_includes_features(self): # The artist did not get songs returned that they were featured in. name = "Swae Lee" diff --git a/tests/test_genius.py b/tests/test_genius.py index d1c3e9ef..5a4cd524 100644 --- a/tests/test_genius.py +++ b/tests/test_genius.py @@ -2,14 +2,12 @@ import vcr -from . import genius, test_vcr +from . import genius class TestEndpoints(unittest.TestCase): @classmethod - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix(' endpoints.yaml'), - serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Endpoint tests...\n") @@ -17,8 +15,6 @@ def setUpClass(cls): cls.song_title_only = "99 Problems" cls.tag = genius.tag('pop') - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml') def test_search_song(self): artist = "Jay-Z" # Empty response @@ -49,7 +45,6 @@ def test_search_song(self): response = genius.search_song(self.song_title_only, artist="Drake") self.assertFalse(response.title.lower() == self.song_title_only.lower()) - @test_vcr.use_cassette def test_song_annotations(self): msg = "Incorrect song annotation response." r = sorted(genius.song_annotations(1)) @@ -96,14 +91,10 @@ def setUpClass(cls): "\nI wonder who you’re thinking of" ) - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml') def test_lyrics_with_url(self): lyrics = genius.lyrics(song_url=self.song_url) self.assertTrue(lyrics.endswith(self.lyrics_ending)) - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix('.yaml'), - serializer='yaml') def test_lyrics_with_id(self): lyrics = genius.lyrics(self.song_id) self.assertTrue(lyrics.endswith(self.lyrics_ending)) diff --git a/tests/test_public_methods.py b/tests/test_public_methods.py index a76f2776..7de4c850 100644 --- a/tests/test_public_methods.py +++ b/tests/test_public_methods.py @@ -1,11 +1,8 @@ import unittest - -import vcr - from lyricsgenius import PublicAPI -from . import test_vcr, genius +from . import genius client = PublicAPI() @@ -19,37 +16,31 @@ def setUpClass(cls): cls.album_id = 104614 - @test_vcr.use_cassette def test_album(self): msg = "Album ID did not match." r = client.album(self.album_id) self.assertEqual(r['album']['id'], self.album_id, msg) - @test_vcr.use_cassette def test_albums_charts(self): msg = "Album charts were empty." r = client.albums_charts() self.assertTrue("chart_items" in r, msg) - @test_vcr.use_cassette def test_album_comments(self): msg = "Album comments were empty." r = client.album_comments(self.album_id) self.assertTrue("comments" in r, msg) - @test_vcr.use_cassette def test_album_cover_arts(self): msg = "Album cover arts were empty." r = client.album_cover_arts(self.album_id) self.assertTrue("cover_arts" in r, msg) - @test_vcr.use_cassette def test_album_leaderboard(self): msg = "Album leaderboard was empty." r = client.album_leaderboard(self.album_id) self.assertTrue("leaderboard" in r, msg) - @test_vcr.use_cassette def test_album_tracks(self): msg = "Album tracks were empty." r = client.album_tracks(self.album_id) @@ -64,20 +55,16 @@ def setUpClass(cls): cls.annotation_id = 10225840 - @test_vcr.use_cassette( - path_transformer=vcr.VCR.ensure_suffix(' public' + '.' + test_vcr.serializer)) def test_annotation(self): msg = "annotation ID did not match." r = client.annotation(self.annotation_id) self.assertEqual(r['annotation']['id'], self.annotation_id, msg) - @test_vcr.use_cassette def test_annotation_edits(self): msg = "annotation edits were empty." r = client.annotation_edits(self.annotation_id) self.assertTrue("versions" in r, msg) - @test_vcr.use_cassette def test_annotation_comments(self): msg = "annotation comments were empty." r = client.annotation_comments(self.annotation_id) @@ -92,19 +79,16 @@ def setUpClass(cls): cls.article_id = 11880 - @test_vcr.use_cassette def test_article(self): msg = "article ID did not match." r = client.article(self.article_id) self.assertEqual(r['article']['id'], self.article_id, msg) - @test_vcr.use_cassette def test_article_comments(self): msg = "article comments were empty." r = client.article_comments(self.article_id) self.assertTrue("comments" in r, msg) - @test_vcr.use_cassette def test_latest_articles(self): msg = "latest articles were empty." r = client.latest_articles() @@ -119,17 +103,14 @@ def setUpClass(cls): cls.artist_id = 1665 - @test_vcr.use_cassette def test_artist(self): r = client.artist(self.artist_id) self.assertEqual(r['artist']['id'], self.artist_id) - @test_vcr.use_cassette def test_artist_activity(self): r = client.artist_activity(self.artist_id) self.assertTrue("line_items" in r) - @test_vcr.use_cassette def test_artist_albums(self): r = client.artist_albums(self.artist_id) self.assertTrue("albums" in r) @@ -139,22 +120,18 @@ def test_artist_albums(self): # r = client.artist_contribution_opportunities(self.artist_id) # self.assertIsNotNone(r.get('contribution_opportunities')) - @test_vcr.use_cassette def test_artist_followers(self): r = client.artist_followers(self.artist_id) self.assertTrue("followers" in r) - @test_vcr.use_cassette def test_artist_leaderboard(self): r = client.artist_leaderboard(self.artist_id) self.assertTrue("leaderboard" in r) - @test_vcr.use_cassette def test_artist_songs(self): r = client.artist_songs(self.artist_id) self.assertTrue("songs" in r) - @test_vcr.use_cassette def test_search_artist_songs(self): r = client.search_artist_songs(self.artist_id, 'test') self.assertTrue("songs" in r) @@ -168,7 +145,6 @@ def setUpClass(cls): cls.album_id = 104614 - @test_vcr.use_cassette def test_cover_arts(self): r = client.cover_arts(self.album_id) self.assertTrue("cover_arts" in r) @@ -182,17 +158,14 @@ def setUpClass(cls): # cls.discussion_id = 123 # -# @test_vcr.use_cassette # def test_discussion(self): # r = client.discussion(self.discussion_id) # self.assertEqual(r['discussion']['id'], self.discussion_id) # -# @test_vcr.use_cassette # def test_discussion_replies(self): # r = client.discussion_replies(self.discussion_id) # self.assertTrue("forum_posts" in r) - @test_vcr.use_cassette def test_discussions(self): r = client.discussions() self.assertTrue("discussions" in r) @@ -204,12 +177,10 @@ class TestLeaderboardMethods(unittest.TestCase): def setUpClass(cls): print("\n---------------------\nSetting up leaerboard methods tests...\n") - @test_vcr.use_cassette def test_leaderboard(self): r = client.leaderboard() self.assertTrue("leaderboard" in r) - @test_vcr.use_cassette def test_charts(self): r = client.charts() self.assertTrue("chart_items" in r) @@ -223,7 +194,6 @@ def setUpClass(cls): cls.album_id = 104614 - @test_vcr.use_cassette def test_questions(self): r = client.questions(self.album_id) self.assertIsNotNone(r.get('questions')) @@ -238,13 +208,11 @@ def setUpClass(cls): cls.web_page_id = 10347 cls.referent_ids = [20793764, 20641014] - @test_vcr.use_cassette def test_referent(self): r = client.referent(self.referent_ids) self.assertTrue(str(self.referent_ids[0]) in r['referents']) self.assertTrue(str(self.referent_ids[1]) in r['referents']) - @test_vcr.use_cassette def test_referents(self): r = client.referents(web_page_id=self.web_page_id) self.assertIsNotNone(r.get('referents')) @@ -258,47 +226,38 @@ def setUpClass(cls): cls.search_term = 'test' - @test_vcr.use_cassette def test_search(self): r = client.search(self.search_term) self.assertIsNotNone(r['hits']) - @test_vcr.use_cassette def test_search_albums(self): r = client.search_albums(self.search_term) self.assertEqual(r['sections'][0]['type'], 'album') - @test_vcr.use_cassette def test_search_articles(self): r = client.search_articles(self.search_term) self.assertEqual(r['sections'][0]['type'], 'article') - @test_vcr.use_cassette def test_search_artists(self): r = client.search_artists(self.search_term) self.assertEqual(r['sections'][0]['type'], 'artist') - @test_vcr.use_cassette def test_search_lyrics(self): r = client.search_lyrics(self.search_term) self.assertEqual(r['sections'][0]['type'], 'lyric') - @test_vcr.use_cassette def test_search_songs(self): r = client.search_songs(self.search_term) self.assertEqual(r['sections'][0]['type'], 'song') - @test_vcr.use_cassette def test_search_users(self): r = client.search_users(self.search_term) self.assertEqual(r['sections'][0]['type'], 'user') - @test_vcr.use_cassette def test_search_videos(self): r = client.search_videos(self.search_term) self.assertEqual(r['sections'][0]['type'], 'video') - @test_vcr.use_cassette def test_search_all(self): r = client.search_all(self.search_term) self.assertEqual(r['sections'][0]['type'], 'top_hit') @@ -312,22 +271,18 @@ def setUpClass(cls): cls.song_id = 378195 - @test_vcr.use_cassette def test_song(self): r = client.song(self.song_id) self.assertEqual(r['song']['id'], self.song_id) - @test_vcr.use_cassette def test_song_activity(self): r = client.song_activity(self.song_id) self.assertTrue("line_items" in r) - @test_vcr.use_cassette def test_song_comments(self): r = client.song_comments(self.song_id) self.assertTrue("comments" in r) - @test_vcr.use_cassette def test_song_contributors(self): r = client.song_contributors(self.song_id) self.assertTrue("contributors" in r) @@ -341,68 +296,56 @@ def setUpClass(cls): cls.user_id = 1 - @test_vcr.use_cassette def test_user(self): r = client.user(self.user_id) self.assertEqual(r['user']['id'], self.user_id) - @test_vcr.use_cassette def test_user_accomplishments(self): r = client.user_accomplishments(self.user_id) self.assertTrue("accomplishments" in r) - @test_vcr.use_cassette def test_user_following(self): r = client.user_following(self.user_id) self.assertTrue("followed_users" in r) - @test_vcr.use_cassette def test_user_followers(self): r = client.user_followers(self.user_id) self.assertTrue("followers" in r) - @test_vcr.use_cassette def test_user_contributions(self): r = client.user_contributions(self.user_id) self.assertTrue("contribution_groups" in r) - @test_vcr.use_cassette def test_user_annotations(self): r = client.user_annotations(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'annotation') - @test_vcr.use_cassette def test_user_articles(self): r = client.user_articles(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'article') - @test_vcr.use_cassette def test_user_pyongs(self): r = client.user_pyongs(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'pyong') - @test_vcr.use_cassette def test_user_questions_and_answers(self): r = client.user_questions_and_answers(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'answer') - @test_vcr.use_cassette def test_user_suggestions(self): r = client.user_suggestions(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'comment') - @test_vcr.use_cassette def test_user_transcriptions(self): r = client.user_transcriptions(self.user_id) type = r['contribution_groups'][0]['contribution_type'] self.assertEqual(type, 'song') - @test_vcr.use_cassette def test_user_unreviewed(self): r = client.user_unreviewed(self.user_id) type = r['contribution_groups'][0]['contribution_type'] @@ -417,12 +360,10 @@ def setUpClass(cls): cls.video_id = 18681 - @test_vcr.use_cassette def test_video(self): r = client.video(self.video_id) self.assertEqual(r['video']['id'], self.video_id) - @test_vcr.use_cassette def test_videos(self): r = client.videos(video_id=self.video_id, series=True) self.assertTrue("video_lists" in r) @@ -445,14 +386,12 @@ def setUpClass(cls): # r = client.line_item(self.line_item_id) # self.assertTrue("line_item" in r) - @test_vcr.use_cassette def test_page_data_album(self): album_path = '/albums/Eminem/Music-to-be-murdered-by' page_data = genius.page_data(album=album_path) self.assertTrue('page_data' in page_data) - @test_vcr.use_cassette def test_page_data_song(self): artist = client.artist(1665) artist_slug = artist['artist']['slug'] @@ -463,7 +402,6 @@ def test_page_data_song(self): page_data = genius.page_data(artist=artist_slug, song=song_path) self.assertTrue('page_data' in page_data) - @test_vcr.use_cassette def test_voters(self): r = client.voters(annotation_id=self.annotation_id) self.assertTrue("voters" in r) diff --git a/tests/test_song.py b/tests/test_song.py index db26bc1b..13845ede 100644 --- a/tests/test_song.py +++ b/tests/test_song.py @@ -1,10 +1,7 @@ import os import unittest - -import vcr - -from . import genius, test_vcr +from . import genius from lyricsgenius.types import Song from lyricsgenius.utils import clean_str @@ -12,8 +9,6 @@ class TestSong(unittest.TestCase): @classmethod - @test_vcr.use_cassette(path_transformer=vcr.VCR.ensure_suffix(' song.yaml'), - serializer='yaml') def setUpClass(cls): print("\n---------------------\nSetting up Song tests...\n") From 96bb0c616a2a777b2f37125a095c1ca3784831f8 Mon Sep 17 00:00:00 2001 From: johnwmillr Date: Sun, 7 Feb 2021 09:52:09 -0800 Subject: [PATCH 40/44] Remove VCR from test_base.py --- tests/test_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index e52ec24d..e20adef7 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -2,7 +2,7 @@ from requests.exceptions import HTTPError -from . import genius, test_vcr +from . import genius class TestAPIBase(unittest.TestCase): @@ -11,7 +11,6 @@ class TestAPIBase(unittest.TestCase): def setUpClass(cls): print("\n---------------------\nSetting up API base tests...\n") - @test_vcr.use_cassette def test_http_error_handler(self): status_code = None try: From 0c59c098ad694cabcfc22eb4aec3b9d5ea6a1859 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Sun, 7 Feb 2021 22:42:07 +0330 Subject: [PATCH 41/44] Remove VCR from test_genius.py and test_public_methods.py --- tests/test_genius.py | 2 -- tests/test_public_methods.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/test_genius.py b/tests/test_genius.py index 5a4cd524..ce73396d 100644 --- a/tests/test_genius.py +++ b/tests/test_genius.py @@ -1,7 +1,5 @@ import unittest -import vcr - from . import genius diff --git a/tests/test_public_methods.py b/tests/test_public_methods.py index 7de4c850..3b87b7e3 100644 --- a/tests/test_public_methods.py +++ b/tests/test_public_methods.py @@ -115,7 +115,6 @@ def test_artist_albums(self): r = client.artist_albums(self.artist_id) self.assertTrue("albums" in r) - # @test_vcr.use_cassette # def test_artist_contribution_opportunities(self): # r = client.artist_contribution_opportunities(self.artist_id) # self.assertIsNotNone(r.get('contribution_opportunities')) @@ -381,7 +380,6 @@ def setUpClass(cls): # cls.line_item_id = 146262999 cls.annotation_id = 10225840 - # @test_vcr.use_cassette # def test_line_item(self): # r = client.line_item(self.line_item_id) # self.assertTrue("line_item" in r) From 184432f75f12088a4ba7ae4890c0b1342dbea148 Mon Sep 17 00:00:00 2001 From: johnwmillr Date: Sun, 7 Feb 2021 15:33:51 -0800 Subject: [PATCH 42/44] Update version to 3.0.0 --- lyricsgenius/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyricsgenius/__init__.py b/lyricsgenius/__init__.py index cc059ada..d1e43053 100644 --- a/lyricsgenius/__init__.py +++ b/lyricsgenius/__init__.py @@ -14,4 +14,4 @@ __url__ = 'https://github.com/johnwmillr/LyricsGenius' __description__ = 'A Python wrapper around the Genius API' __license__ = 'MIT' -__version__ = '2.0.2' +__version__ = '3.0.0' From 4c7f312d36167ffd05c802210279197fcc713d49 Mon Sep 17 00:00:00 2001 From: Allerter <45076212+Allerter@users.noreply.github.com> Date: Mon, 8 Feb 2021 14:07:41 +0330 Subject: [PATCH 43/44] added release notes for 3.0.0 --- docs/src/release_notes.rst | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/src/release_notes.rst b/docs/src/release_notes.rst index 1222886a..a04db5fd 100644 --- a/docs/src/release_notes.rst +++ b/docs/src/release_notes.rst @@ -3,6 +3,56 @@ Release notes ============= +3.0.0 (2021-02-??) +------------------ +New +*** + +- All requests now go through the ``Sender`` object. This provides + features such as retries ``genius.retries`` and handling HTTP and + timeout errors. For more info have a look at the guide about `request + error handling`_. +- Added ``OAuth2`` class to help with OAuth2 authentication. +- Added ``PublicAPI`` class to allow accessing methods of the public + API (genius.com/api). Check `this page`_ for a list of available + methods. +- Added the ``Album`` type and the ``genius.search_album()`` method. +- Added the ``genius.tag()`` method to get songs by tag. +- All API endpoints are now supported (e.g. ``upvote_annotation``). +- New additions to the docs. + +Changed +******* + +- ``GENIUS_CLIENT_ACCESS_TOKEN`` env var has been renamed to + ``GENIUS_ACCESS_TOKEN``. +- ``genius.client_access_token`` has been renamed to + ``genius.access_token``. +- ``genius.search_song()`` will also accept ``song_id``. +- Lyrics won't be fetched for instrumental songs and their lyrics will + be set to ``""``. You can check to see if a song is instrumental + using ``Song.instrumental``. +- Renamed all interface methods to remove redundant ``get_`` + (``genius.get_song`` is now ``genius.song``). +- Renamed the lyrics method to ``genius.lyrics()`` to allow use by + users. It accepts song URLs and song IDs. +- Reformatted the types. Some attributes won't be available anymore. + More info on the `types page`_. +- ``save_lyrics()`` will save songs with ``utf8`` encoding when + ``extension='txt'``. +- Using ``Genius()`` will check for the env var + ``GENIUS_ACCESS_TOKEN``. + +Other (CI, etc) +*************** + +- Bumped ``Sphinx`` to 3.3.0 + +.. _request error handling: https://lyricsgenius.readthedocs.io/en/master/other_guides.html#request-errors +.. _this page: https://lyricsgenius.readthedocs.io/en/latest/reference/genius.html +.. _types page: https://lyricsgenius.readthedocs.io/en/latest/reference/types.html#types + + 2.0.2 (2020-09-26) ------------------ Added @@ -15,6 +65,7 @@ Added :meth:`Artist.save_lyrics ` and :meth:`Artist.to_json ` + 2.0.1 (2020-09-20) ------------------ Changed From ab719c0099f3e9d828b4f317018ce8dd55c8af9e Mon Sep 17 00:00:00 2001 From: johnwmillr Date: Mon, 8 Feb 2021 18:00:55 -0800 Subject: [PATCH 44/44] Set release date to 2021-02-08 --- docs/src/release_notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/release_notes.rst b/docs/src/release_notes.rst index a04db5fd..926ff8aa 100644 --- a/docs/src/release_notes.rst +++ b/docs/src/release_notes.rst @@ -3,7 +3,7 @@ Release notes ============= -3.0.0 (2021-02-??) +3.0.0 (2021-02-08) ------------------ New ***