Browse Source

Implemented first api calls

Johann Schmitz 1 year ago
parent
commit
5b9f83c2e3
Signed by: Johann Schmitz <johann@j-schmitz.net> GPG Key ID: A084064277C501ED
8 changed files with 286 additions and 0 deletions
  1. 1
    0
      .gitignore
  2. 1
    0
      requirements.txt
  3. 2
    0
      requirements_dev.txt
  4. 2
    0
      tests/__init__.py
  5. 71
    0
      tests/test_language.py
  6. 89
    0
      tests/test_login.py
  7. 3
    0
      tvdbrest/__init__.py
  8. 117
    0
      tvdbrest/client.py

+ 1
- 0
.gitignore View File

@@ -96,3 +96,4 @@ ENV/
96 96
 .idea
97 97
 dev_settings.py*
98 98
 testresults.xml
99
+dummy.py

+ 1
- 0
requirements.txt View File

@@ -1 +1,2 @@
1 1
 # Project dependencies
2
+requests

+ 2
- 0
requirements_dev.txt View File

@@ -1,3 +1,5 @@
1 1
 # Project dependencies for development
2 2
 pytest
3 3
 coveralls
4
+mock
5
+coverage

+ 2
- 0
tests/__init__.py View File

@@ -0,0 +1,2 @@
1
+# -*- coding: utf-8 -*-
2
+

+ 71
- 0
tests/test_language.py View File

@@ -0,0 +1,71 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import mock
4
+import pytest
5
+
6
+from tvdbrest.client import TVDB, Unauthorized, NotFound, Language
7
+
8
+
9
+@pytest.fixture
10
+def tvdb():
11
+    tvdb = TVDB("myusername", "myuserkey", "myapikey")
12
+    tvdb.jwttoken = "test-token"
13
+    return tvdb
14
+
15
+
16
+def api_response_mock(json):
17
+    m = mock.MagicMock()
18
+    m.status_code = 200
19
+    m.json = mock.Mock(return_value=json)
20
+    return m
21
+
22
+
23
+def api_response_404_mock():
24
+    m = mock.MagicMock()
25
+    m.status_code = 404
26
+    return m
27
+
28
+
29
+class TestAPI(object):
30
+
31
+    @mock.patch('tvdbrest.client.requests.request')
32
+    def test_languages(self, request_method_mock, tvdb):
33
+        request_method_mock.return_value = api_response_mock({
34
+            "data": [
35
+                {
36
+                    "id": 27,
37
+                    "abbreviation": "zh",
38
+                    "name": "中文",
39
+                    "englishName": "Chinese"
40
+                },
41
+                {
42
+                    "id": 7,
43
+                    "abbreviation": "en",
44
+                    "name": "English",
45
+                    "englishName": "English"
46
+                }
47
+        ]})
48
+        
49
+        languages = tvdb.languages()
50
+        assert languages
51
+        assert all(isinstance(x, Language) for x in languages)
52
+
53
+    @mock.patch('tvdbrest.client.requests.request')
54
+    def test_get_language(self, request_method_mock, tvdb):
55
+        request_method_mock.return_value = api_response_mock({
56
+            "id": 27,
57
+            "abbreviation": "zh",
58
+            "name": "中文",
59
+            "englishName": "Chinese"
60
+        })
61
+    
62
+        language = tvdb.language(27)
63
+        assert language
64
+        assert language.id == 27
65
+
66
+    @mock.patch('tvdbrest.client.requests.request')
67
+    def test_get_language_does_not_exist(self, request_method_mock, tvdb):
68
+        request_method_mock.return_value = api_response_404_mock()
69
+    
70
+        with pytest.raises(NotFound):
71
+            tvdb.language(42)

+ 89
- 0
tests/test_login.py View File

@@ -0,0 +1,89 @@
1
+# -*- coding: utf-8 -*-
2
+import mock
3
+import pytest
4
+
5
+from tvdbrest.client import TVDB, Unauthorized
6
+
7
+
8
+@pytest.fixture
9
+def tvdb():
10
+    return TVDB("myusername", "myuserkey", "myapikey")
11
+
12
+
13
+@pytest.fixture
14
+def empty_positive_response():
15
+    m = mock.Mock()
16
+    m.status_code = 200
17
+    m.json = mock.Mock(return_value={"data": []})
18
+    return m
19
+
20
+
21
+class TestLoginLogout(object):
22
+    
23
+    def test_login_status(self):
24
+        tvdb = TVDB("myusername", "myuserkey", "myapikey")
25
+        assert not tvdb.logged_in
26
+
27
+    @mock.patch('tvdbrest.client.requests.request')
28
+    def test_login_status_after_login(self, request_mock):
29
+        response_mock = mock.MagicMock()
30
+        response_mock.status_code = 200
31
+        response_mock.json = mock.MagicMock(return_value={
32
+            'token': 'jwttoken'
33
+        })
34
+        
35
+        request_mock.return_value = response_mock
36
+        
37
+        tvdb = TVDB("myusername", "myuserkey", "myapikey")
38
+        tvdb.login()
39
+        
40
+        request_mock.assert_called_with('post', 'https://api.thetvdb.com/login', headers={}, json={
41
+            'username': 'myusername',
42
+            'userkey': 'myuserkey',
43
+            'apikey': 'myapikey'
44
+        })
45
+
46
+        assert tvdb.logged_in
47
+
48
+    @mock.patch('tvdbrest.client.requests.request')
49
+    def test_failed_login(self, request_method_mock):
50
+        response_mock = mock.MagicMock()
51
+        response_mock.status_code = 401
52
+        request_method_mock.return_value = response_mock
53
+        
54
+        tvdb = TVDB("myusername", "myuserkey", "myapikey")
55
+        with pytest.raises(Unauthorized):
56
+            tvdb.login()
57
+        
58
+        assert not tvdb.logged_in
59
+
60
+    def test_logout(self, tvdb):
61
+        tvdb.jwttoken = "abc"
62
+        assert tvdb.logged_in
63
+        tvdb.logout()
64
+        assert tvdb.jwttoken is None
65
+        assert not tvdb.logged_in
66
+
67
+    @mock.patch('tvdbrest.client.requests.request')
68
+    def test_decorator_login_before_api_call(self, request_method_mock, tvdb):
69
+        response_mock = mock.MagicMock()
70
+        response_mock.status_code = 200
71
+        response_mock.json = mock.MagicMock(return_value={"data": []})
72
+
73
+        request_method_mock.return_value = response_mock
74
+
75
+        login_mock = mock.MagicMock()
76
+        tvdb.login = login_mock
77
+        tvdb.languages()
78
+        
79
+        assert login_mock.called
80
+
81
+    @mock.patch('tvdbrest.client.requests.request')
82
+    def test_authorization_for_api_call(self, request_mock, tvdb, empty_positive_response):
83
+        request_mock.return_value = empty_positive_response
84
+        tvdb.jwttoken = "test"
85
+        tvdb.languages()
86
+    
87
+        request_mock.assert_called_with('get', 'https://api.thetvdb.com/languages', headers={
88
+            'Authorization': 'Bearer test'
89
+        })

+ 3
- 0
tvdbrest/__init__.py View File

@@ -0,0 +1,3 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+VERSION = "0.1"

+ 117
- 0
tvdbrest/client.py View File

@@ -0,0 +1,117 @@
1
+# -*- coding: utf-8 -*-
2
+import logging
3
+from functools import wraps
4
+from urllib.parse import urljoin
5
+
6
+import requests
7
+
8
+from tvdbrest import VERSION
9
+
10
+logger = logging.getLogger(__name__)
11
+
12
+
13
+class Unauthorized(Exception):
14
+    pass
15
+
16
+
17
+class NotFound(Exception):
18
+    pass
19
+
20
+
21
+class APIError(Exception):
22
+    pass
23
+
24
+
25
+class APIObject(object):
26
+    def __init__(self, attrs):
27
+        self._attrs = attrs
28
+    
29
+    def __getattr__(self, item):
30
+        return self._attrs[item]
31
+
32
+    def __eq__(self, other):
33
+        return isinstance(other, self.__class__) and self.id == other.id
34
+
35
+
36
+class Language(APIObject):
37
+    pass
38
+
39
+
40
+def login_required(f):
41
+    @wraps(f)
42
+    def wrapper(obj, *args, **kwargs):
43
+        if not obj.logged_in:
44
+            logger.debug("not logged in")
45
+            obj.login()
46
+
47
+        try:
48
+            return f(obj, *args, **kwargs)
49
+        except Unauthorized:
50
+            logger.info("Unauthorized API error - login again")
51
+            obj.login()
52
+            return f(obj, *args, **kwargs)
53
+    
54
+    return wrapper
55
+
56
+
57
+class TVDB(object):
58
+    
59
+    def __init__(self, username, userkey, apikey):
60
+        self.username = username
61
+        self.userkey = userkey
62
+        self.apikey = apikey
63
+        
64
+        assert self.username and self.userkey and self.apikey
65
+        self.jwttoken = None
66
+        
67
+        self.useragent = "tvdb-rest %s" % VERSION
68
+
69
+    def login(self):
70
+        self.jwttoken = None
71
+        response = self._api_request('post', '/login', json={
72
+            'username': self.username,
73
+            'userkey': self.userkey,
74
+            'apikey': self.apikey,
75
+        })
76
+        
77
+        self.jwttoken = response['token']
78
+    
79
+    def logout(self):
80
+        self.jwttoken = None
81
+    
82
+    @property
83
+    def logged_in(self):
84
+        return self.jwttoken is not None
85
+    
86
+    @login_required
87
+    def languages(self):
88
+        return self._api_request('get', '/languages', response_class=Language, many=True)
89
+    
90
+    @login_required
91
+    def language(self, id):
92
+        return self._api_request('get', '/languages/%s' % id, response_class=Language)
93
+    
94
+    def _api_request(self, method, relative_url, response_class=None, many=False, **kwargs):
95
+
96
+        url = urljoin('https://api.thetvdb.com/', relative_url)
97
+
98
+        headers = kwargs.pop('headers', {})
99
+        if self.jwttoken:
100
+            headers['Authorization'] = 'Bearer %s' % self.jwttoken
101
+
102
+        response = requests.request(method, url, headers=headers, **kwargs)
103
+        
104
+        if response.status_code == 401:
105
+            raise Unauthorized()
106
+        elif response.status_code == 404:
107
+            raise NotFound()
108
+        elif response.status_code >= 400:
109
+            raise APIError()
110
+        
111
+        logger.info("Response: %s", response)
112
+        if response_class:
113
+            if many:
114
+                return [response_class(d) for d in response.json()['data']]
115
+            return response_class(response.json())
116
+        
117
+        return response.json()