Repository: eadwinCode/shoutcast_api

Issue:
Caching requests
Hello @eadwinCode,

I noticed you are using [python-requests](https://requests.readthedocs.io/).
I think you can quite easily implement a way to cache queries using [requests-cache](https://github.com/reclosedev/requests-cache)

See how it's being used in https://pandas-datareader.readthedocs.io/en/latest/cache.html passing `session=None` by default or passing `session=requests_cache.CachedSession(...)`

See also
https://github.com/pydata/pandas-datareader/blob/adb67b2e959f2060a9db18f8b343d06e917f23da/pandas_datareader/_utils.py#L56-L60

Kind regards

Discussion:

Patch:
diff --git a/shoutcast_api/genres.py b/shoutcast_api/genres.py
index 020f8e2..d9d02f4 100755
--- a/shoutcast_api/genres.py
+++ b/shoutcast_api/genres.py
@@ -4,9 +4,9 @@ from .models import Genre, GenreList
 from .utils import genre_xml_strip
 
 
-def _handle_url_action_json(url):
+def _handle_url_action_json(endpoint, session):
     list_genre = []
-    response = shoutcast_request.call_api_json(url)
+    response = shoutcast_request.call_api_json(endpoint, session=session)
 
     genrelist = response.get('genrelist')
 
@@ -18,15 +18,15 @@ def _handle_url_action_json(url):
     return GenreList(list_genre)
 
 
-def get_all_genres(k):
+def get_all_genres(k, session=None):
     """
     Get all the genres on SHOUTcast Radio Directory
     :param k: API Dev ID
     :return: `class GenreList()`
     """
     list_genre = []
-    url = "legacy/genrelist?k={}".format(k)
-    response = shoutcast_request.call_api_xml(url)
+    endpoint = "/legacy/genrelist?k={}".format(k)
+    response = shoutcast_request.call_api_xml(endpoint, session=session)
 
     genrelist = response.get('genrelist')
 
@@ -38,19 +38,19 @@ def get_all_genres(k):
     return GenreList(list_genre)
 
 
-def get_primary_genres_json(k):
+def get_primary_genres_json(k, session=None):
     """
     Get only the Primary Genres on SHOUTcast Radio Directory
     :param k: API Dev ID
     :return: `class GenreList()`
     """
 
-    url = "genre/primary?k={}&f=json".format(k)
+    endpoint = "/genre/primary?k={}&f=json".format(k)
 
-    return _handle_url_action_json(url)
+    return _handle_url_action_json(endpoint, session)
 
 
-def get_secondary_genres_json(k, parentid: int = 0):
+def get_secondary_genres_json(k, parentid: int = 0, session=None):
     """
     Get secondary genre list (if present) for a specified primary genre.
     :param parentid: Genreid of the primary genre. You can retrieve the entire genre set by passing parentid=0.
@@ -58,12 +58,12 @@ def get_secondary_genres_json(k, parentid: int = 0):
     :return: `class GenreList()`
     """
 
-    url = "genre/secondary?k={}&f=json".format(k)
-    url += "&parentid={}".format(parentid)
-    return _handle_url_action_json(url)
+    endpoint = "/genre/secondary?k={}&f=json".format(k)
+    endpoint += "&parentid={}".format(parentid)
+    return _handle_url_action_json(endpoint, session=session)
 
 
-def get_genres_details_by_id(k, genre_id: int = None) -> Genre:
+def get_genres_details_by_id(k, genre_id: int = None, session=None) -> Genre:
     """
     Get details such as Genre Name, Sub Genres (if its a primary genre), has children by passing the genre-id.
     :param genre_id: Input respective genre or sub-genre id.
@@ -73,8 +73,8 @@ def get_genres_details_by_id(k, genre_id: int = None) -> Genre:
     if not genre_id:
         raise Exception('id is required')
 
-    url = "genre/secondary?k={}&f=json&id={}".format(k, genre_id)
-    response = shoutcast_request.call_api_json(url)
+    endpoint = "/genre/secondary?k={}&f=json&id={}".format(k, genre_id)
+    response = shoutcast_request.call_api_json(endpoint, session=session)
 
     genrelist = response.get('genrelist')
 
@@ -84,7 +84,7 @@ def get_genres_details_by_id(k, genre_id: int = None) -> Genre:
     return Genre(genrelist.get('genre'))
 
 
-def get_genres_by_sub_genres(k, haschildren: bool = False):
+def get_genres_by_sub_genres(k, haschildren: bool = False, session=None):
     """
     Get genres based on their sub-genre availability at any node level in the genre hierarchy of SHOUTcast.
     :param haschildren: Input respective genre or sub-genre id.
@@ -95,10 +95,10 @@ def get_genres_by_sub_genres(k, haschildren: bool = False):
     :return: `class GenreList()`
     """
 
-    url = "genre/secondary?k={}&f=json".format(k)
+    endpoint = "/genre/secondary?k={}&f=json".format(k)
     if haschildren:
-        url += '&haschildren=true'
+        endpoint += '&haschildren=true'
     else:
-        url += '&haschildren=false'
+        endpoint += '&haschildren=false'
 
-    return _handle_url_action_json(url)
+    return _handle_url_action_json(endpoint, session)
diff --git a/shoutcast_api/shoutcast_request.py b/shoutcast_api/shoutcast_request.py
index a1e7c86..e04da5e 100755
--- a/shoutcast_api/shoutcast_request.py
+++ b/shoutcast_api/shoutcast_request.py
@@ -1,18 +1,19 @@
 import xmltodict
 import json
-from requests import get
 from .models import Tunein
-
+from .utils import _init_session
 from .Exceptions import APIException
-base_url = 'http://api.shoutcast.com/'
+
+base_url = 'http://api.shoutcast.com'
 tunein_url = 'http://yp.shoutcast.com/{base}?id={id}'
 
 tuneins = [Tunein('/sbin/tunein-station.pls'), Tunein('/sbin/tunein-station.m3u'), Tunein('/sbin/tunein-station.xspf')]
 
 
-def call_api_xml(url):
-    request_url = "{}{}".format(base_url, url)
-    response = get(request_url)
+def call_api_xml(endpoint, params=None, session=None):
+    session = _init_session(session)
+    request_url = "{}{}".format(base_url, endpoint)
+    response = session.get(request_url, params=params)
     if response.status_code == 200:
         response_as_dict = xmltodict.parse(response.content)
         api_response = response_as_dict.get('response')
@@ -28,9 +29,10 @@ def call_api_xml(url):
     raise APIException(response.content, code=response.status_code)
 
 
-def call_api_json(url):
-    request_url = "{}{}".format(base_url, url)
-    response = get(request_url)
+def call_api_json(endpoint, params=None, session=None):
+    session = _init_session(session)
+    request_url = "{}{}".format(base_url, endpoint)
+    response = session.get(request_url, params=params)
     if response.status_code == 200:
         json_response = json.loads(response.content.decode('utf-8'))
 
@@ -47,18 +49,20 @@ def call_api_json(url):
     raise APIException(response.reason, code=response.status_code)
 
 
-def call_api_tunein(station_id: int):
+def call_api_tunein(station_id: int, session=None):
+    session = _init_session(session)
     url = tunein_url.format(base=tuneins[2], id=station_id)
-    response = get(url)
+    response = session.get(url)
     if response.status_code == 200:
         api_response = xmltodict.parse(response.content.decode('utf-8'))
         return api_response
     raise APIException(response.reason, code=response.status_code)
 
 
-def call_api_tunein_any(base: Tunein, station_id: int):
+def call_api_tunein_any(base: Tunein, station_id: int, session=None):
+    session = _init_session(session)
     url = tunein_url.format(base=base, id=station_id)
-    response = get(url)
+    response = session.get(url)
     if response.status_code == 200:
         return response.content.decode('utf-8')
     raise APIException(response.reason, code=response.status_code)
diff --git a/shoutcast_api/stations.py b/shoutcast_api/stations.py
index 5f9bf96..9c9d41d 100755
--- a/shoutcast_api/stations.py
+++ b/shoutcast_api/stations.py
@@ -1,12 +1,13 @@
+from requests.sessions import session
 from shoutcast_api import shoutcast_request
 from typing import Tuple, AnyStr, List
 from .models import Station, StationList
 from .utils import _build_url, station_xml_strip, station_json_strip
 
 
-def _handle_url_action_xml(url: str):
+def _handle_url_action_xml(endpoint: str, session):
     stations = list()
-    response = shoutcast_request.call_api_xml(url)
+    response = shoutcast_request.call_api_xml(endpoint, session=session)
     api_station_list = response.get('stationlist')
 
     if not api_station_list.get('station'):
@@ -22,9 +23,9 @@ def _handle_url_action_xml(url: str):
     return StationList(tunein=shoutcast_request.tuneins, stations=stations)
 
 
-def _handle_url_action_json(url: str) -> StationList:
+def _handle_url_action_json(endpoint: str, session) -> StationList:
     stations = list()
-    response = shoutcast_request.call_api_json(url)
+    response = shoutcast_request.call_api_json(endpoint, session=session)
     api_station_list = response.get('stationlist')
 
     if not api_station_list.get('station'):
@@ -40,7 +41,7 @@ def _handle_url_action_json(url: str) -> StationList:
     return StationList(tunein=shoutcast_request.tuneins, stations=stations)
 
 
-def get_top_500(k: AnyStr, limit: (int, Tuple) = None, **kwargs) -> StationList:
+def get_top_500(k: AnyStr, limit: (int, Tuple) = None, session=None, **kwargs) -> StationList:
     """
     gets top 500 stations from shoutcast api
     :param k: API Dev Key.
@@ -52,13 +53,13 @@ def get_top_500(k: AnyStr, limit: (int, Tuple) = None, **kwargs) -> StationList:
     :return: list of stations
     """
 
-    url = '/legacy/Top500?k={}'.format(k)
-    url += _build_url(limit=limit, **kwargs)
+    endpoint = '/legacy/Top500?k={}'.format(k)
+    endpoint += _build_url(limit=limit, **kwargs)
 
-    return _handle_url_action_xml(url)
+    return _handle_url_action_xml(endpoint, session=session)
 
 
-def get_stations_keywords(k, search: str, limit: (int, Tuple) = None, **kwargs) -> StationList:
+def get_stations_keywords(k, search: str, limit: (int, Tuple) = None, session=None, **kwargs) -> StationList:
     """
         Get stations which match the keyword searched on SHOUTcast Radio Directory.
        :param search: Specify the query to search
@@ -73,13 +74,13 @@ def get_stations_keywords(k, search: str, limit: (int, Tuple) = None, **kwargs)
     if not search:
         raise Exception('Search query is required')
 
-    url = "legacy/stationsearch?k={}&search={}".format(k, search.replace(' ', '+').strip())
-    url += _build_url(limit, **kwargs)
+    endpoint = "/legacy/stationsearch?k={}&search={}".format(k, search.replace(' ', '+').strip())
+    endpoint += _build_url(limit, **kwargs)
 
-    return _handle_url_action_xml(url)
+    return _handle_url_action_xml(endpoint, session=session)
 
 
-def get_stations_by_genre(k, genre: str, limit: (int, Tuple) = None, **kwargs) -> StationList:
+def get_stations_by_genre(k, genre: str, limit: (int, Tuple) = None, session=None, **kwargs) -> StationList:
     """
        Get stations which match the genre specified as query.
        :param genre: genre
@@ -94,15 +95,15 @@ def get_stations_by_genre(k, genre: str, limit: (int, Tuple) = None, **kwargs) -
     if not genre:
         raise Exception('genre is required')
 
-    url = "legacy/stationsearch?k={}&search={}".format(
+    endpoint = "/legacy/stationsearch?k={}&search={}".format(
         k, genre.replace(' ', '+').strip()
     )
-    url += _build_url(limit, **kwargs)
+    endpoint += _build_url(limit, **kwargs)
 
-    return _handle_url_action_xml(url)
+    return _handle_url_action_xml(endpoint, session=session)
 
 
-def get_stations_by_now_playing(k, ct: str, limit: (int, Tuple) = None, **kwargs) -> StationList:
+def get_stations_by_now_playing(k, ct: str, limit: (int, Tuple) = None, session=None, **kwargs) -> StationList:
     """
        Return stations which match a specified query in the now playing node.
        :param ct: Query to search in Now Playing node. This parameter also supports querying multiple artists in the same query by using "||". ex: ct=madonna||u2||beyonce up to 10 artists
@@ -117,16 +118,16 @@ def get_stations_by_now_playing(k, ct: str, limit: (int, Tuple) = None, **kwargs
     if not ct:
         raise Exception('genre is required')
 
-    url = "station/nowplaying?k={}&ct={}&f=json".format(
+    endpoint = "/station/nowplaying?k={}&ct={}&f=json".format(
         k, ct.replace(' ', '+').strip()
     )
-    url += _build_url(limit, **kwargs)
+    endpoint += _build_url(limit, **kwargs)
 
-    return _handle_url_action_json(url)
+    return _handle_url_action_json(endpoint, session=session)
 
 
 def get_stations_bitrate_or_genre_id(k, br: int = 128,
-                                     genre_id: int = None, limit: (int, Tuple) = None, **kwargs) -> StationList:
+                                     genre_id: int = None, limit: (int, Tuple) = None, session=None, **kwargs) -> StationList:
     """
           Get stations which match the genre specified as query.
           :param genre_id: genre id
@@ -142,13 +143,13 @@ def get_stations_bitrate_or_genre_id(k, br: int = 128,
     if not br and not genre_id:
         raise Exception('genre_id or br is required')
 
-    url = "station/advancedsearch?k={}&f=json".format(k)
-    url += _build_url(limit, br=br, genre_id=genre_id, **kwargs)
+    endpoint = "/station/advancedsearch?k={}&f=json".format(k)
+    endpoint += _build_url(limit, br=br, genre_id=genre_id, **kwargs)
 
-    return _handle_url_action_json(url)
+    return _handle_url_action_json(endpoint, session=session)
 
 
-def get_random_station(k, limit: (int, Tuple) = None, **kwargs):
+def get_random_station(k, limit: (int, Tuple) = None, session=None, **kwargs):
     """
           Get random stations on SHOUTcast Radio Directory. Random stations can be restricted
           to the Bitrate/Genre/Media type specified.
@@ -162,7 +163,7 @@ def get_random_station(k, limit: (int, Tuple) = None, **kwargs):
           :return: `List[Stations]`
        """
 
-    url = "station/randomstations?k={}&f=json".format(k)
-    url += _build_url(limit, **kwargs)
+    endpoint = "/station/randomstations?k={}&f=json".format(k)
+    endpoint += _build_url(limit, **kwargs)
 
-    return _handle_url_action_json(url)
+    return _handle_url_action_json(endpoint, session=session)
diff --git a/shoutcast_api/tunein.py b/shoutcast_api/tunein.py
index ff1eddd..c7b30b0 100755
--- a/shoutcast_api/tunein.py
+++ b/shoutcast_api/tunein.py
@@ -3,14 +3,14 @@ from .models import Track, TrackList, Tunein
 from .shoutcast_request import call_api_tunein, call_api_tunein_any
 
 
-def get_stations_stream_url(station_id: int) -> TrackList:
+def get_stations_stream_url(station_id: int, session=None) -> TrackList:
     """
     Get station streaming url as List[Track]
     :param station_id: shoutcast station id
     :return: class `TrackList`
     """
     tracks = []
-    response = call_api_tunein(station_id)
+    response = call_api_tunein(station_id, session=session)
     playlist = response.get('playlist')
     api_track_list = playlist.get('trackList')
 
@@ -25,7 +25,7 @@ def get_stations_stream_url(station_id: int) -> TrackList:
     return TrackList(tracks)
 
 
-def tunein_to_station(base: Tunein, station_id: int) -> str:
+def tunein_to_station(base: Tunein, station_id: int, session=None) -> str:
     """
 
     :param base: value is taken from the tunein node and based on the playlist format required
@@ -33,4 +33,4 @@ def tunein_to_station(base: Tunein, station_id: int) -> str:
     :param station_id: station id
     :return: str
     """
-    return call_api_tunein_any(base, station_id)
+    return call_api_tunein_any(base, station_id, session=session)
diff --git a/shoutcast_api/utils.py b/shoutcast_api/utils.py
index c3b152e..811f99d 100755
--- a/shoutcast_api/utils.py
+++ b/shoutcast_api/utils.py
@@ -1,3 +1,4 @@
+import requests
 from typing import Tuple
 
 
@@ -68,3 +69,9 @@ def genre_xml_strip(genre):
     item['name'] = genre.get('@name')
     item['count'] = int(genre.get('@count'))
     return item
+
+
+def _init_session(session):
+    if session is None:
+        session = requests.Session()
+    return session


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 0000000..0ac0ed9
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,14 @@
+
+from unittest import TestCase
+
+
+class BaseTestCase(TestCase):
+    def setUp(self):
+        try:
+            import requests_cache
+            import datetime
+
+            expire_after = datetime.timedelta(days=3)
+            self.session = requests_cache.CachedSession(cache_name='cache', backend='sqlite', expire_after=expire_after)
+        except ImportError:
+            self.session = None
diff --git a/tests/test_genres.py b/tests/test_genres.py
index 267a8fa..c04d902 100755
--- a/tests/test_genres.py
+++ b/tests/test_genres.py
@@ -1,5 +1,5 @@
 import os
-from unittest import TestCase
+from .test_base import BaseTestCase
 from shoutcast_api.genres import (
  get_all_genres, get_primary_genres_json, get_genres_details_by_id,
  get_genres_by_sub_genres, GenreList, Genre, get_secondary_genres_json
@@ -8,31 +8,31 @@ from shoutcast_api.genres import (
 api_key = os.getenv('SHOUTCAST_API_KEY')
 
 
-class TestGenre(TestCase):
+class TestGenre(BaseTestCase):
     def test_get_all_genres(self):
-        response = get_all_genres(api_key)
+        response = get_all_genres(api_key, session=self.session)
         self.assertIsInstance(response, GenreList)
 
     def test_get_primary_genres_json(self):
-        response = get_primary_genres_json(api_key)
+        response = get_primary_genres_json(api_key, session=self.session)
         self.assertIsInstance(response, GenreList)
 
     def test_get_genres_details_by_id(self):
-        response = get_genres_details_by_id(api_key, genre_id=25)
+        response = get_genres_details_by_id(api_key, genre_id=25, session=self.session)
         self.assertIsInstance(response, Genre)
         self.assertEqual(response.id, 25)
 
     def test_get_genres_by_sub_genres_haschildren_false_return_genre_with_haschildren_false(self):
-        response = get_genres_by_sub_genres(api_key, haschildren=False)
+        response = get_genres_by_sub_genres(api_key, haschildren=False, session=self.session)
         self.assertIsInstance(response, GenreList)
         self.assertEqual(response.genres[0].haschildren, False)
 
     def test_get_genres_by_sub_genres_haschildren_true_return_genre_with_haschildren_true(self):
-        response = get_genres_by_sub_genres(api_key, haschildren=True)
+        response = get_genres_by_sub_genres(api_key, haschildren=True, session=self.session)
         self.assertIsInstance(response, GenreList)
         self.assertEqual(response.genres[0].haschildren, True)
 
     def test_get_secondary_genres_json(self):
-        response = get_secondary_genres_json(api_key, parentid=1)
+        response = get_secondary_genres_json(api_key, parentid=1, session=self.session)
         self.assertIsInstance(response, GenreList)
         self.assertEqual(response.genres[0].parentid, 1)
diff --git a/tests/test_stations.py b/tests/test_stations.py
index cac0ebf..28a9339 100755
--- a/tests/test_stations.py
+++ b/tests/test_stations.py
@@ -1,5 +1,5 @@
 import os
-from unittest import TestCase
+from .test_base import BaseTestCase
 from shoutcast_api.stations import (
     StationList, get_random_station, get_stations_keywords, get_stations_by_genre,
     get_stations_bitrate_or_genre_id, get_stations_by_now_playing, get_top_500
@@ -8,35 +8,34 @@ from shoutcast_api.stations import (
 api_key = os.getenv('SHOUTCAST_API_KEY')
 
 
-class TestStations(TestCase):
+class TestStations(BaseTestCase):
 
     def test_get_top_500(self):
-        response = get_top_500(api_key, limit=5, br=128)
+        response = get_top_500(api_key, limit=5, br=128, session=self.session)
         self.assertIsInstance(response, StationList)
         self.assertEqual(response.station[0].br, 128)
 
     def test_get_stations_by_now_playing(self):
-        response = get_stations_by_now_playing(api_key, ct='john legend', limit=2)
+        response = get_stations_by_now_playing(api_key, ct='john legend', limit=2, session=self.session)
         self.assertIsInstance(response, StationList)
         if len(response.station) > 0:
             self.assertTrue('john legend' in response.station[0].ct.lower())
 
     def test_get_stations_bitrate_or_genre_id(self):
-        response = get_stations_bitrate_or_genre_id(api_key, br=128, genre_id=25, limit=2)
+        response = get_stations_bitrate_or_genre_id(api_key, br=128, genre_id=25, limit=2, session=self.session)
         self.assertIsInstance(response, StationList)
         self.assertEqual(response.station[0].br, 128)
 
     def test_get_stations_by_genre(self):
-        response = get_stations_by_genre(api_key, genre='hip hop', limit=2)
+        response = get_stations_by_genre(api_key, genre='hip hop', limit=2, session=self.session)
         self.assertIsInstance(response, StationList)
         self.assertTrue('hip hop' in response.station[0].genre.lower())
 
     def test_get_stations_keywords(self):
-        response = get_stations_keywords(api_key, search='Hot', br=128, limit=2)
+        response = get_stations_keywords(api_key, search='Hot', br=128, limit=2, session=self.session)
         self.assertIsInstance(response, StationList)
         self.assertEqual(response.station[0].br, 128)
 
     def test_get_random_station(self):
-        response = get_random_station(api_key, limit=2)
+        response = get_random_station(api_key, limit=2, session=self.session)
         self.assertIsInstance(response, StationList)
-
diff --git a/tests/test_tunein.py b/tests/test_tunein.py
index b56952c..21b8654 100755
--- a/tests/test_tunein.py
+++ b/tests/test_tunein.py
@@ -1,18 +1,18 @@
-from unittest import TestCase
+from .test_base import BaseTestCase
 from shoutcast_api.tunein import (
     Track, TrackList, get_stations_stream_url, tunein_to_station
 )
 from shoutcast_api.shoutcast_request import tuneins
 
 
-class TestTunein(TestCase):
+class TestTunein(BaseTestCase):
     def test_get_stations_stream_url(self):
-        response = get_stations_stream_url(station_id=99311623)
+        response = get_stations_stream_url(station_id=99311623, session=self.session)
         self.assertIsInstance(response, TrackList)
         if len(response.tracks) > 0:
             track = response.tracks[0]
             self.assertIsInstance(track, Track)
 
     def test_tunein_to_station(self):
-        response = tunein_to_station(base=tuneins[0], station_id=99466001)
+        response = tunein_to_station(base=tuneins[0], station_id=99466001, session=self.session)
         self.assertIsInstance(response, str)