From 09183fce92bf259ad61886dbb84b833a4dca79fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 09:57:10 +0200 Subject: [PATCH 001/193] PoC: New api client --- trakt/api.py | 224 ++++++++++++++++++++++++++++++++++++++++++++++++ trakt/core.py | 19 ++++ trakt/errors.py | 20 +++++ 3 files changed, 263 insertions(+) create mode 100644 trakt/api.py diff --git a/trakt/api.py b/trakt/api.py new file mode 100644 index 00000000..8986ee56 --- /dev/null +++ b/trakt/api.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +"""Interfaces to all of the People objects offered by the Trakt.tv API""" +import json +import logging +import os +from datetime import datetime, timezone, timedelta +from functools import lru_cache, wraps +from typing import NamedTuple, List, Optional +from urllib.parse import urljoin + +from trakt import errors +from requests import Session + +__author__ = 'Jon Nappi, Elan Ruusamäe' + +from trakt.errors import Errors + + +class TraktApiParameters(NamedTuple): + BASE_URL: str + CLIENT_ID: Optional[str] + CLIENT_SECRET: Optional[str] + OAUTH_EXPIRES_AT: Optional[int] + OAUTH_REFRESH: Optional[int] + OAUTH_TOKEN: Optional[str] + OAUTH_TOKEN_VALID: Optional[bool] + REDIRECT_URI: str + HEADERS: Optional[dict[str, str]] + + +class TraktApiTokenAuth(dict): + """Class dealing with loading and updating oauth refresh token. + """ + + def __init__(self, params: TraktApiParameters, session: Session, error_map): + super().__init__() + self.CONFIG_PATH = None + self.update(**params._asdict()) + self.session = session + self.error_map = error_map + self.logger = logging.getLogger('trakt.api.oauth') + + def bootstrap(self): + """Bootstrap your authentication environment when authentication is + needed and if a file at `CONFIG_PATH` exists. + The process is completed by setting the client id header. + """ + + # global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT + # global OAUTH_REFRESH, OAUTH_TOKEN + + self.load_config() + # Check token validity and refresh token if needed + if (not self['OAUTH_TOKEN_VALID'] and self['OAUTH_EXPIRES_AT'] is not None + and self['OAUTH_REFRESH'] is not None): + self.validate_token(self) + # For backwards compatibility with trakt<=2.3.0 + # if api_key is not None and OAUTH_TOKEN is None: + # OAUTH_TOKEN = api_key + + return [ + self['CLIENT_ID'], + self['OAUTH_TOKEN'], + ] + + def validate_token(self): + """Check if current OAuth token has not expired""" + # global OAUTH_TOKEN_VALID + current = datetime.now(tz=timezone.utc) + expires_at = datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) + if expires_at - current > timedelta(days=2): + self['OAUTH_TOKEN_VALID'] = True + else: + self.refresh_token(self) + + def refresh_token(self): + """Request Trakt API for a new valid OAuth token using refresh_token""" + # global OAUTH_TOKEN, OAUTH_EXPIRES_AT, OAUTH_REFRESH, OAUTH_TOKEN_VALID + self.logger.info("OAuth token has expired, refreshing now...") + url = urljoin(self['BASE_URL'], '/oauth/token') + data = { + 'client_id': self['CLIENT_ID'], + 'client_secret': self['CLIENT_SECRET'], + 'refresh_token': self['OAUTH_REFRESH'], + 'redirect_uri': self['REDIRECT_URI'], + 'grant_type': 'refresh_token' + } + response = self.session.post(url, json=data, headers=self['HEADERS']) + self.logger.debug('RESPONSE [post] (%s): %s', url, str(response)) + if response.status_code == 200: + data = response.json() + self['OAUTH_TOKEN'] = data.get("access_token") + self['OAUTH_REFRESH'] = data.get("refresh_token") + self['OAUTH_EXPIRES_AT'] = data.get("created_at") + data.get("expires_in") + self['OAUTH_TOKEN_VALID'] = True + self.logger.info( + "OAuth token successfully refreshed, valid until {}".format( + datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) + ) + ) + self.store_token( + CLIENT_ID=self['CLIENT_ID'], CLIENT_SECRET=self['CLIENT_SECRET'], + OAUTH_TOKEN=self['OAUTH_TOKEN'], OAUTH_REFRESH=self['OAUTH_REFRESH'], + OAUTH_EXPIRES_AT=self['OAUTH_EXPIRES_AT'] + ) + elif response.status_code == 401: + self.logger.debug( + "Rejected - Unable to refresh expired OAuth token, " + "refresh_token is invalid" + ) + elif response.status_code in self.error_map: + raise self.error_map[response.status_code](response) + + def store_token(self, **kwargs): + """Helper function used to store Trakt configurations at ``CONFIG_PATH`` + + :param kwargs: Keyword args to store at ``CONFIG_PATH`` + """ + with open(CONFIG_PATH, 'w') as config_file: + json.dump(kwargs, config_file) + + def load_config(self): + """Manually load config from json config file.""" + # global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_EXPIRES_AT + # global OAUTH_REFRESH, APPLICATION_ID, CONFIG_PATH + if (self['CLIENT_ID'] is None or self['CLIENT_SECRET'] is None) and \ + os.path.exists(self.CONFIG_PATH): + # Load in trakt API auth data from CONFIG_PATH + with open(self.CONFIG_PATH) as config_file: + config_data = json.load(config_file) + + if self['CLIENT_ID'] is None: + self['CLIENT_ID'] = config_data.get('CLIENT_ID', None) + if self['CLIENT_SECRET'] is None: + self['CLIENT_SECRET'] = config_data.get('CLIENT_SECRET', None) + if self['OAUTH_TOKEN'] is None: + self['OAUTH_TOKEN'] = config_data.get('OAUTH_TOKEN', None) + if self['OAUTH_EXPIRES_AT'] is None: + self['OAUTH_EXPIRES_AT'] = config_data.get('OAUTH_EXPIRES_AT', None) + if self['OAUTH_REFRESH'] is None: + self['OAUTH_REFRESH'] = config_data.get('OAUTH_REFRESH', None) + if self['APPLICATION_ID'] is None: + self['APPLICATION_ID'] = config_data.get('APPLICATION_ID', None) + + +class TraktApi: + """This class contains all of the functionality required for interfacing + with the Trakt.tv API + """ + + def __init__(self, session: Session, params: TraktApiParameters): + self.BASE_URL = params.BASE_URL + self.session = session + self.token_auth = TraktApiTokenAuth(params=params, session=session, error_map=self.error_map) + self.logger = logging.getLogger('trakt.api') + + def get(self, url: str): + return self.request('get', url) + + def delete(self, url: str): + self.request('delete', self.BASE_URL + url) + + def post(self, url: str, data): + return self.request('post', url, data=data) + + def put(self, url: str, data): + return self.request('put', url, data=data) + + def request(self, method, url, data=None): + """Handle actually talking out to the trakt API, logging out debug + information, raising any relevant `TraktException` Exception types, + and extracting and returning JSON data + + :param method: The HTTP method we're executing on. Will be one of + post, put, delete, get + :param url: The fully qualified url to send our request to + :param data: Optional data payload to send to the API + :return: The decoded JSON response from the Trakt API + :raises TraktException: If any non-200 return code is encountered + """ + + headers = self.prepare() + self.logger.debug('%s: %s', method, url) + self.logger.debug('method, url :: %s, %s', method, url) + if method == 'get': # GETs need to pass data as params, not body + response = self.session.request(method, url, headers=headers, params=data) + else: + response = self.session.request(method, url, headers=headers, data=json.dumps(data)) + self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) + if response.status_code == 204: # HTTP no content + return None + self.raise_if_needed(response) + json_data = json.loads(response.content.decode('UTF-8', 'ignore')) + return json_data + + @lru_cache(maxsize=None) + def prepare(self): + [client_id, client_token] = self.token_auth.bootstrap() + + headers = { + 'trakt-api-key': client_id, + 'Authorization': f'Bearer {client_token}', + } + self.logger.debug('headers: %s', str(headers)) + + return headers + + @property + @lru_cache(maxsize=1) + def error_map(self): + """Map HTTP response codes to exception types + """ + + # Get all of our exceptions except the base exception + errs = [getattr(errors, att) for att in errors.__all__ + if att != 'TraktException'] + + return {err.http_code: err for err in errs} + + def raise_if_needed(self, response): + if response.status_code in self.error_map: + raise self.error_map[response.status_code](response) + + diff --git a/trakt/core.py b/trakt/core.py index dd3f8931..d33fc98e 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -367,6 +367,25 @@ def init(*args, **kwargs): 'updated_at', 'likes', 'user_rating']) +def get_config(): + global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT + global OAUTH_REFRESH, OAUTH_TOKEN + global CLIENT_ID, CLIENT_SECRET + + from trakt.api import TraktApiParameters + return TraktApiParameters( + BASE_URL=BASE_URL, + CLIENT_ID=CLIENT_ID, + CLIENT_SECRET=CLIENT_SECRET, + OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, + OAUTH_REFRESH=OAUTH_REFRESH, + OAUTH_TOKEN=OAUTH_TOKEN, + OAUTH_TOKEN_VALID=OAUTH_TOKEN_VALID, + REDIRECT_URI=REDIRECT_URI, + HEADERS=HEADERS, + ) + + def _validate_token(s): """Check if current OAuth token has not expired""" global OAUTH_TOKEN_VALID diff --git a/trakt/errors.py b/trakt/errors.py index 8a54cd48..c9408e46 100644 --- a/trakt/errors.py +++ b/trakt/errors.py @@ -20,6 +20,26 @@ 'TraktUnavailable', ] +from functools import lru_cache + + +class Errors: + def raise_if_needed(self, response): + if response.status_code in self.error_map: + raise self.error_map[response.status_code](response) + + @lru_cache(maxsize=None) + def error_map(self): + """Map HTTP response codes to exception types + """ + import sys + module = sys.modules[__name__] + # Get all of our exceptions except the base exception + errs = [getattr(module, att) for att in __all__ + if att != 'TraktException'] + + return {err.http_code: err for err in errs} + class TraktException(Exception): """Base Exception type for trakt module""" From 6b6a8ec1d4bfd4effb0d647c8deca9e3e06f95fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 10:07:10 +0200 Subject: [PATCH 002/193] fixup! PoC: New api client --- trakt/api.py | 66 ++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 8986ee56..ee081fba 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -13,7 +13,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' -from trakt.errors import Errors +from trakt.errors import Errors, TraktException, OAuthException class TraktApiParameters(NamedTuple): @@ -32,12 +32,11 @@ class TraktApiTokenAuth(dict): """Class dealing with loading and updating oauth refresh token. """ - def __init__(self, params: TraktApiParameters, session: Session, error_map): + def __init__(self, client: 'TraktApi', params: TraktApiParameters): super().__init__() + self.client = client self.CONFIG_PATH = None self.update(**params._asdict()) - self.session = session - self.error_map = error_map self.logger = logging.getLogger('trakt.api.oauth') def bootstrap(self): @@ -46,14 +45,11 @@ def bootstrap(self): The process is completed by setting the client id header. """ - # global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT - # global OAUTH_REFRESH, OAUTH_TOKEN - self.load_config() # Check token validity and refresh token if needed if (not self['OAUTH_TOKEN_VALID'] and self['OAUTH_EXPIRES_AT'] is not None and self['OAUTH_REFRESH'] is not None): - self.validate_token(self) + self.validate_token() # For backwards compatibility with trakt<=2.3.0 # if api_key is not None and OAUTH_TOKEN is None: # OAUTH_TOKEN = api_key @@ -65,17 +61,17 @@ def bootstrap(self): def validate_token(self): """Check if current OAuth token has not expired""" - # global OAUTH_TOKEN_VALID + current = datetime.now(tz=timezone.utc) expires_at = datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) if expires_at - current > timedelta(days=2): self['OAUTH_TOKEN_VALID'] = True else: - self.refresh_token(self) + self.refresh_token() def refresh_token(self): """Request Trakt API for a new valid OAuth token using refresh_token""" - # global OAUTH_TOKEN, OAUTH_EXPIRES_AT, OAUTH_REFRESH, OAUTH_TOKEN_VALID + self.logger.info("OAuth token has expired, refreshing now...") url = urljoin(self['BASE_URL'], '/oauth/token') data = { @@ -85,38 +81,38 @@ def refresh_token(self): 'redirect_uri': self['REDIRECT_URI'], 'grant_type': 'refresh_token' } - response = self.session.post(url, json=data, headers=self['HEADERS']) - self.logger.debug('RESPONSE [post] (%s): %s', url, str(response)) - if response.status_code == 200: - data = response.json() - self['OAUTH_TOKEN'] = data.get("access_token") - self['OAUTH_REFRESH'] = data.get("refresh_token") - self['OAUTH_EXPIRES_AT'] = data.get("created_at") + data.get("expires_in") - self['OAUTH_TOKEN_VALID'] = True - self.logger.info( - "OAuth token successfully refreshed, valid until {}".format( - datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) - ) - ) - self.store_token( - CLIENT_ID=self['CLIENT_ID'], CLIENT_SECRET=self['CLIENT_SECRET'], - OAUTH_TOKEN=self['OAUTH_TOKEN'], OAUTH_REFRESH=self['OAUTH_REFRESH'], - OAUTH_EXPIRES_AT=self['OAUTH_EXPIRES_AT'] - ) - elif response.status_code == 401: + + try: + response = self.client.post(url, data) + except OAuthException: self.logger.debug( "Rejected - Unable to refresh expired OAuth token, " "refresh_token is invalid" ) - elif response.status_code in self.error_map: - raise self.error_map[response.status_code](response) + return + + self['OAUTH_TOKEN'] = response.get("access_token") + self['OAUTH_REFRESH'] = response.get("refresh_token") + self['OAUTH_EXPIRES_AT'] = response.get("created_at") + response.get("expires_in") + self['OAUTH_TOKEN_VALID'] = True + + self.logger.info( + "OAuth token successfully refreshed, valid until {}".format( + datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) + ) + ) + self.store_token( + CLIENT_ID=self['CLIENT_ID'], CLIENT_SECRET=self['CLIENT_SECRET'], + OAUTH_TOKEN=self['OAUTH_TOKEN'], OAUTH_REFRESH=self['OAUTH_REFRESH'], + OAUTH_EXPIRES_AT=self['OAUTH_EXPIRES_AT'], + ) def store_token(self, **kwargs): """Helper function used to store Trakt configurations at ``CONFIG_PATH`` :param kwargs: Keyword args to store at ``CONFIG_PATH`` """ - with open(CONFIG_PATH, 'w') as config_file: + with open(self.CONFIG_PATH, 'w') as config_file: json.dump(kwargs, config_file) def load_config(self): @@ -151,7 +147,7 @@ class TraktApi: def __init__(self, session: Session, params: TraktApiParameters): self.BASE_URL = params.BASE_URL self.session = session - self.token_auth = TraktApiTokenAuth(params=params, session=session, error_map=self.error_map) + self.token_auth = TraktApiTokenAuth(params=params, client=self) self.logger = logging.getLogger('trakt.api') def get(self, url: str): @@ -220,5 +216,3 @@ def error_map(self): def raise_if_needed(self, response): if response.status_code in self.error_map: raise self.error_map[response.status_code](response) - - From 25ab87d1479ef4881acee735d363ca24ec242727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 10:17:14 +0200 Subject: [PATCH 003/193] Update --- trakt/api.py | 139 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 55 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index ee081fba..086a6d1b 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -28,21 +28,87 @@ class TraktApiParameters(NamedTuple): HEADERS: Optional[dict[str, str]] +class HttpClient: + """Class for abstracting HTTP requests + """ + + def __init__(self, base_url: str, session: Session): + self.base_url = base_url + self.session = session + self.logger = logging.getLogger('trakt.http_client') + self.headers = {} + + def get(self, url: str): + return self.request('get', url) + + def delete(self, url: str): + self.request('delete', self.base_url + url) + + def post(self, url: str, data): + return self.request('post', url, data=data) + + def put(self, url: str, data): + return self.request('put', url, data=data) + + def request(self, method, url, data=None): + """Handle actually talking out to the trakt API, logging out debug + information, raising any relevant `TraktException` Exception types, + and extracting and returning JSON data + + :param method: The HTTP method we're executing on. Will be one of + post, put, delete, get + :param url: The fully qualified url to send our request to + :param data: Optional data payload to send to the API + :return: The decoded JSON response from the Trakt API + :raises TraktException: If any non-200 return code is encountered + """ + + self.logger.debug('%s: %s', method, url) + self.logger.debug('method, url :: %s, %s', method, url) + if method == 'get': # GETs need to pass data as params, not body + response = self.session.request(method, url, headers=self.headers, params=data) + else: + response = self.session.request(method, url, headers=self.headers, data=json.dumps(data)) + self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) + if response.status_code == 204: # HTTP no content + return None + self.raise_if_needed(response) + json_data = json.loads(response.content.decode('UTF-8', 'ignore')) + return json_data + + @property + @lru_cache(maxsize=1) + def error_map(self): + """Map HTTP response codes to exception types + """ + + # Get all of our exceptions except the base exception + errs = [getattr(errors, att) for att in errors.__all__ + if att != 'TraktException'] + + return {err.http_code: err for err in errs} + + def raise_if_needed(self, response): + if response.status_code in self.error_map: + raise self.error_map[response.status_code](response) + + def set_headers(self, headers): + self.headers.update(headers) + + class TraktApiTokenAuth(dict): """Class dealing with loading and updating oauth refresh token. """ - def __init__(self, client: 'TraktApi', params: TraktApiParameters): + def __init__(self, client: HttpClient, params: TraktApiParameters): super().__init__() self.client = client self.CONFIG_PATH = None self.update(**params._asdict()) self.logger = logging.getLogger('trakt.api.oauth') - def bootstrap(self): - """Bootstrap your authentication environment when authentication is - needed and if a file at `CONFIG_PATH` exists. - The process is completed by setting the client id header. + def get_token(self): + """Return client_id, client_token pair needed for Trakt.tv authentication """ self.load_config() @@ -147,72 +213,35 @@ class TraktApi: def __init__(self, session: Session, params: TraktApiParameters): self.BASE_URL = params.BASE_URL self.session = session - self.token_auth = TraktApiTokenAuth(params=params, client=self) + self.client = HttpClient(params.BASE_URL, session) + self.token_auth = TraktApiTokenAuth(params=params, client=self.client) self.logger = logging.getLogger('trakt.api') def get(self, url: str): - return self.request('get', url) + self.authorize() + return self.client.get(url) def delete(self, url: str): - self.request('delete', self.BASE_URL + url) + self.authorize() + self.client.delete(url) def post(self, url: str, data): - return self.request('post', url, data=data) + self.authorize() + return self.client.post(url, data=data) def put(self, url: str, data): - return self.request('put', url, data=data) - - def request(self, method, url, data=None): - """Handle actually talking out to the trakt API, logging out debug - information, raising any relevant `TraktException` Exception types, - and extracting and returning JSON data - - :param method: The HTTP method we're executing on. Will be one of - post, put, delete, get - :param url: The fully qualified url to send our request to - :param data: Optional data payload to send to the API - :return: The decoded JSON response from the Trakt API - :raises TraktException: If any non-200 return code is encountered - """ - - headers = self.prepare() - self.logger.debug('%s: %s', method, url) - self.logger.debug('method, url :: %s, %s', method, url) - if method == 'get': # GETs need to pass data as params, not body - response = self.session.request(method, url, headers=headers, params=data) - else: - response = self.session.request(method, url, headers=headers, data=json.dumps(data)) - self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) - if response.status_code == 204: # HTTP no content - return None - self.raise_if_needed(response) - json_data = json.loads(response.content.decode('UTF-8', 'ignore')) - return json_data + self.authorize() + return self.client.put(url, data=data) @lru_cache(maxsize=None) - def prepare(self): - [client_id, client_token] = self.token_auth.bootstrap() + def authorize(self): + [client_id, client_token] = self.token_auth.get_token() headers = { 'trakt-api-key': client_id, 'Authorization': f'Bearer {client_token}', } self.logger.debug('headers: %s', str(headers)) + self.client.set_headers(headers) return headers - - @property - @lru_cache(maxsize=1) - def error_map(self): - """Map HTTP response codes to exception types - """ - - # Get all of our exceptions except the base exception - errs = [getattr(errors, att) for att in errors.__all__ - if att != 'TraktException'] - - return {err.http_code: err for err in errs} - - def raise_if_needed(self, response): - if response.status_code in self.error_map: - raise self.error_map[response.status_code](response) From 2b7ab970427442385da172b3e7170944f54beaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 10:18:02 +0200 Subject: [PATCH 004/193] fixup! Update --- trakt/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 086a6d1b..fa22045e 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -13,7 +13,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' -from trakt.errors import Errors, TraktException, OAuthException +from trakt.errors import OAuthException class TraktApiParameters(NamedTuple): From a7b6efa01fbc9f11c778a3e81bed104c1d450a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 10:18:47 +0200 Subject: [PATCH 005/193] fixup! fixup! Update --- trakt/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index fa22045e..ca25709a 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -50,6 +50,9 @@ def post(self, url: str, data): def put(self, url: str, data): return self.request('put', url, data=data) + def set_headers(self, headers): + self.headers.update(headers) + def request(self, method, url, data=None): """Handle actually talking out to the trakt API, logging out debug information, raising any relevant `TraktException` Exception types, @@ -92,9 +95,6 @@ def raise_if_needed(self, response): if response.status_code in self.error_map: raise self.error_map[response.status_code](response) - def set_headers(self, headers): - self.headers.update(headers) - class TraktApiTokenAuth(dict): """Class dealing with loading and updating oauth refresh token. From 071ae07fda6434f8418117d4ed7f46a019db9f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 10:19:24 +0200 Subject: [PATCH 006/193] fixup! fixup! fixup! Update --- trakt/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index ca25709a..cf2a8e0f 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -79,6 +79,10 @@ def request(self, method, url, data=None): json_data = json.loads(response.content.decode('UTF-8', 'ignore')) return json_data + def raise_if_needed(self, response): + if response.status_code in self.error_map: + raise self.error_map[response.status_code](response) + @property @lru_cache(maxsize=1) def error_map(self): @@ -91,10 +95,6 @@ def error_map(self): return {err.http_code: err for err in errs} - def raise_if_needed(self, response): - if response.status_code in self.error_map: - raise self.error_map[response.status_code](response) - class TraktApiTokenAuth(dict): """Class dealing with loading and updating oauth refresh token. From dd8c18edf690f23bd19229b80aca8b88e98e837d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 10:29:47 +0200 Subject: [PATCH 007/193] fixup! fixup! fixup! fixup! Update --- trakt/api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index cf2a8e0f..4d58e779 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -211,10 +211,8 @@ class TraktApi: """ def __init__(self, session: Session, params: TraktApiParameters): - self.BASE_URL = params.BASE_URL - self.session = session self.client = HttpClient(params.BASE_URL, session) - self.token_auth = TraktApiTokenAuth(params=params, client=self.client) + self.token_auth = TraktApiTokenAuth(client=self.client, params=params) self.logger = logging.getLogger('trakt.api') def get(self, url: str): From b3eca0b86df03aa671553c7a0dbc9bc3be1d1e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:15:32 +0200 Subject: [PATCH 008/193] fixup! fixup! fixup! fixup! fixup! Update --- trakt/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 4d58e779..69c8a360 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -210,8 +210,8 @@ class TraktApi: with the Trakt.tv API """ - def __init__(self, session: Session, params: TraktApiParameters): - self.client = HttpClient(params.BASE_URL, session) + def __init__(self, client: HttpClient, params: TraktApiParameters): + self.client = client self.token_auth = TraktApiTokenAuth(client=self.client, params=params) self.logger = logging.getLogger('trakt.api') From f73905e65b3c81eb2b2679a97d78fe84292d935b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:15:43 +0200 Subject: [PATCH 009/193] Add api factory --- trakt/core.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/trakt/core.py b/trakt/core.py index d33fc98e..89131f07 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -632,10 +632,19 @@ def inner(*args, **kwargs): return inner +def get_api(): + from trakt.api import HttpClient, TraktApi + + params = get_config() + client = HttpClient(BASE_URL, session) + api = TraktApi(client, params) + + return api + # Here we can simplify the code in each module by exporting these instance # method decorators as if they were simple functions. CORE = Core() get = CORE.get post = CORE.post delete = CORE.delete -put = CORE.put +put = CORE.put \ No newline at end of file From 8136350037301d88cdc07b82e44a4b5f051bc03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:16:21 +0200 Subject: [PATCH 010/193] fixup! Add api factory --- trakt/core.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 89131f07..51435872 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -367,24 +367,6 @@ def init(*args, **kwargs): 'updated_at', 'likes', 'user_rating']) -def get_config(): - global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT - global OAUTH_REFRESH, OAUTH_TOKEN - global CLIENT_ID, CLIENT_SECRET - - from trakt.api import TraktApiParameters - return TraktApiParameters( - BASE_URL=BASE_URL, - CLIENT_ID=CLIENT_ID, - CLIENT_SECRET=CLIENT_SECRET, - OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, - OAUTH_REFRESH=OAUTH_REFRESH, - OAUTH_TOKEN=OAUTH_TOKEN, - OAUTH_TOKEN_VALID=OAUTH_TOKEN_VALID, - REDIRECT_URI=REDIRECT_URI, - HEADERS=HEADERS, - ) - def _validate_token(s): """Check if current OAuth token has not expired""" @@ -632,6 +614,25 @@ def inner(*args, **kwargs): return inner +def get_config(): + global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT + global OAUTH_REFRESH, OAUTH_TOKEN + global CLIENT_ID, CLIENT_SECRET + + from trakt.api import TraktApiParameters + return TraktApiParameters( + BASE_URL=BASE_URL, + CLIENT_ID=CLIENT_ID, + CLIENT_SECRET=CLIENT_SECRET, + OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, + OAUTH_REFRESH=OAUTH_REFRESH, + OAUTH_TOKEN=OAUTH_TOKEN, + OAUTH_TOKEN_VALID=OAUTH_TOKEN_VALID, + REDIRECT_URI=REDIRECT_URI, + HEADERS=HEADERS, + ) + + def get_api(): from trakt.api import HttpClient, TraktApi From 7949a0c458c4d2ea6a6b57e25121f74deaf46ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:18:51 +0200 Subject: [PATCH 011/193] Add api test --- tests/test_api.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/test_api.py diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..c0e7446a --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +"""trakt.tv functional tests""" +from trakt.api import TraktApi +from trakt.core import get_api + + +def test_api(): + api = get_api() + assert isinstance(api, TraktApi) From e00e3d0f848f4498717c8f8f5e00fa94f4eb9362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:20:11 +0200 Subject: [PATCH 012/193] Add decorators module --- trakt/decorators.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 trakt/decorators.py diff --git a/trakt/decorators.py b/trakt/decorators.py new file mode 100644 index 00000000..da631d0b --- /dev/null +++ b/trakt/decorators.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +"""Decorators to handle http methods magically""" + +__author__ = 'Jon Nappi, Elan Ruusamäe' From cf74de7d2f55632365b2b0cabaf33091677ce35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:22:17 +0200 Subject: [PATCH 013/193] Always set base url --- trakt/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 69c8a360..bc67ed79 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -42,7 +42,7 @@ def get(self, url: str): return self.request('get', url) def delete(self, url: str): - self.request('delete', self.base_url + url) + self.request('delete', url) def post(self, url: str, data): return self.request('post', url, data=data) @@ -66,6 +66,7 @@ def request(self, method, url, data=None): :raises TraktException: If any non-200 return code is encountered """ + url = self.base_url + url self.logger.debug('%s: %s', method, url) self.logger.debug('method, url :: %s, %s', method, url) if method == 'get': # GETs need to pass data as params, not body From d04c085146cf5409d203151937a52514e518ea4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:25:49 +0200 Subject: [PATCH 014/193] fixup! Add decorators module --- trakt/decorators.py | 110 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/trakt/decorators.py b/trakt/decorators.py index da631d0b..3ffc5fbb 100644 --- a/trakt/decorators.py +++ b/trakt/decorators.py @@ -2,3 +2,113 @@ """Decorators to handle http methods magically""" __author__ = 'Jon Nappi, Elan Ruusamäe' + +from functools import wraps + + +def _get_first(f, *args, **kwargs): + """Extract the first value from the provided generator function *f* + + :param f: A generator function to extract data from + :param args: Non keyword args for the generator function + :param kwargs: Keyword args for the generator function + :return: The full url for the resource, a generator, and either a data + payload or `None` + """ + generator = f(*args, **kwargs) + uri = next(generator) + if not isinstance(uri, (str, tuple)): + # Allow properties to safely yield arbitrary data + return uri + if isinstance(uri, tuple): + uri, data = uri + return uri, generator, data + else: + return uri, generator, None + + +def get(f): + """Perform a HTTP GET request using the provided uri yielded from the + *f* co-routine. The processed JSON results are then sent back to the + co-routine for post-processing, the results of which are then returned + + :param f: Generator co-routine that yields uri, args, and processed + results + :return: The results of the generator co-routine + """ + + @wraps(f) + def inner(*args, **kwargs): + resp = _get_first(f, *args, **kwargs) + if not isinstance(resp, tuple): + # Handle cached property responses + return resp + url, generator, _ = resp + json_data = f.api.get(url) + try: + return generator.send(json_data) + except StopIteration: + return None + + return inner + + +def delete(f): + """Perform an HTTP DELETE request using the provided uri + + :param f: Function that returns a uri to delete to + """ + + @wraps(f) + def inner(*args, **kwargs): + generator = f(*args, **kwargs) + url = next(generator) + f.api.delete(url) + + return inner + + +def post(f): + """Perform an HTTP POST request using the provided uri and optional + args yielded from the *f* co-routine. The processed JSON results are + then sent back to the co-routine for post-processing, the results of + which are then returned + + :param f: Generator co-routine that yields uri, args, and processed + results + :return: The results of the generator co-routine + """ + + @wraps(f) + def inner(*args, **kwargs): + url, generator, data = _get_first(f, *args, **kwargs) + json_data = f.api.post(url, data) + try: + return generator.send(json_data) + except StopIteration: + return None + + return inner + + +def put(f): + """Perform an HTTP PUT request using the provided uri and optional args + yielded from the *f* co-routine. The processed JSON results are then + sent back to the co-routine for post-processing, the results of which + are then returned + + :param f: Generator co-routine that yields uri, args, and processed + results + :return: The results of the generator co-routine + """ + + @wraps(f) + def inner(*args, **kwargs): + url, generator, data = _get_first(f, *args, **kwargs) + json_data = f.api.put(url, data) + try: + return generator.send(json_data) + except StopIteration: + return None + + return inner From 2d62830ddc8cc5139fee23363720701e8511a5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:26:03 +0200 Subject: [PATCH 015/193] fixup! fixup! Add decorators module --- trakt/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/decorators.py b/trakt/decorators.py index 3ffc5fbb..60ad56dd 100644 --- a/trakt/decorators.py +++ b/trakt/decorators.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Decorators to handle http methods magically""" +"""Decorators to handle HTTP methods magically""" __author__ = 'Jon Nappi, Elan Ruusamäe' From 6683794cd595acfd55384bdf0b700e77c71c87af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:28:04 +0200 Subject: [PATCH 016/193] Test tvshow --- tests/test_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index c0e7446a..0d2d649d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,8 +2,12 @@ """trakt.tv functional tests""" from trakt.api import TraktApi from trakt.core import get_api +from trakt.tv import TVShow def test_api(): api = get_api() assert isinstance(api, TraktApi) + + show = TVShow('Game of Thrones') + assert show.title == 'Game of Thrones' From 51d979236d62235bde78f7fc26b6d456114571ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:29:20 +0200 Subject: [PATCH 017/193] Integrate new_get --- trakt/tv.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/trakt/tv.py b/trakt/tv.py index 9c60d9f1..85653094 100644 --- a/trakt/tv.py +++ b/trakt/tv.py @@ -3,6 +3,7 @@ from collections import namedtuple from datetime import datetime, timedelta from trakt.core import Airs, Alias, Comment, Genre, delete, get +from trakt.decorators import get as new_get from trakt.errors import NotFoundException from trakt.sync import (Scrobbler, rate, comment, add_to_collection, add_to_watchlist, add_to_history, remove_from_history, @@ -241,6 +242,13 @@ def _get(self): data['airs'] = Airs(**data['airs']) self._build(data) + @new_get + def new_get(self): + data = yield self.ext_full + data['first_aired'] = airs_date(data['first_aired']) + data['airs'] = Airs(**data['airs']) + self._build(data) + def _build(self, data): extract_ids(data) for key, val in data.items(): From 05e76c6116c660174df8f70e9d5fff74b3a44ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:30:14 +0200 Subject: [PATCH 018/193] Test new api --- tests/test_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 0d2d649d..c88569e2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,3 +11,6 @@ def test_api(): show = TVShow('Game of Thrones') assert show.title == 'Game of Thrones' + + show.api = api + show.new_get() From cbd7e7b56cea19a2bbab68619cc2fe54101588d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:32:17 +0200 Subject: [PATCH 019/193] fixup! Integrate new_get --- trakt/tv.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/trakt/tv.py b/trakt/tv.py index 85653094..c997d5fe 100644 --- a/trakt/tv.py +++ b/trakt/tv.py @@ -245,9 +245,7 @@ def _get(self): @new_get def new_get(self): data = yield self.ext_full - data['first_aired'] = airs_date(data['first_aired']) - data['airs'] = Airs(**data['airs']) - self._build(data) + return data def _build(self, data): extract_ids(data) From 1facdefb870d56ef6fb4d2665d234e64c293c26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 11:34:06 +0200 Subject: [PATCH 020/193] fixup! fixup! Add decorators module --- trakt/decorators.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/trakt/decorators.py b/trakt/decorators.py index 60ad56dd..f4cd2f0b 100644 --- a/trakt/decorators.py +++ b/trakt/decorators.py @@ -44,7 +44,8 @@ def inner(*args, **kwargs): # Handle cached property responses return resp url, generator, _ = resp - json_data = f.api.get(url) + api = args[0].api + json_data = api.get(url) try: return generator.send(json_data) except StopIteration: @@ -63,7 +64,8 @@ def delete(f): def inner(*args, **kwargs): generator = f(*args, **kwargs) url = next(generator) - f.api.delete(url) + api = args[0].api + api.delete(url) return inner @@ -82,7 +84,8 @@ def post(f): @wraps(f) def inner(*args, **kwargs): url, generator, data = _get_first(f, *args, **kwargs) - json_data = f.api.post(url, data) + api = args[0].api + json_data = api.post(url, data) try: return generator.send(json_data) except StopIteration: @@ -105,7 +108,8 @@ def put(f): @wraps(f) def inner(*args, **kwargs): url, generator, data = _get_first(f, *args, **kwargs) - json_data = f.api.put(url, data) + api = args[0].api + json_data = api.put(url, data) try: return generator.send(json_data) except StopIteration: From 9a7640dab87f032be4d6ec4bffd1f8c3a77e0819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 12:39:08 +0200 Subject: [PATCH 021/193] Use api global --- trakt/decorators.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/trakt/decorators.py b/trakt/decorators.py index f4cd2f0b..b6335e5f 100644 --- a/trakt/decorators.py +++ b/trakt/decorators.py @@ -27,6 +27,11 @@ def _get_first(f, *args, **kwargs): return uri, generator, None +def api(): + from trakt.core import get_api + return get_api() + + def get(f): """Perform a HTTP GET request using the provided uri yielded from the *f* co-routine. The processed JSON results are then sent back to the @@ -44,8 +49,7 @@ def inner(*args, **kwargs): # Handle cached property responses return resp url, generator, _ = resp - api = args[0].api - json_data = api.get(url) + json_data = api().get(url) try: return generator.send(json_data) except StopIteration: @@ -64,8 +68,7 @@ def delete(f): def inner(*args, **kwargs): generator = f(*args, **kwargs) url = next(generator) - api = args[0].api - api.delete(url) + api().delete(url) return inner @@ -84,8 +87,7 @@ def post(f): @wraps(f) def inner(*args, **kwargs): url, generator, data = _get_first(f, *args, **kwargs) - api = args[0].api - json_data = api.post(url, data) + json_data = api().post(url, data) try: return generator.send(json_data) except StopIteration: @@ -108,8 +110,7 @@ def put(f): @wraps(f) def inner(*args, **kwargs): url, generator, data = _get_first(f, *args, **kwargs) - api = args[0].api - json_data = api.put(url, data) + json_data = api().put(url, data) try: return generator.send(json_data) except StopIteration: From d54a152bfa5736853a195fc75273bf11ca5e0dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 12:39:30 +0200 Subject: [PATCH 022/193] fixup! Test new api --- tests/test_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index c88569e2..adea056b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,5 +12,4 @@ def test_api(): show = TVShow('Game of Thrones') assert show.title == 'Game of Thrones' - show.api = api show.new_get() From ee9f76d003efe2a6a2fa90d958703aba2ea7d853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 12:42:38 +0200 Subject: [PATCH 023/193] fixup! fixup! Integrate new_get --- trakt/tv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/tv.py b/trakt/tv.py index c997d5fe..9a2b06ae 100644 --- a/trakt/tv.py +++ b/trakt/tv.py @@ -245,7 +245,7 @@ def _get(self): @new_get def new_get(self): data = yield self.ext_full - return data + yield data def _build(self, data): extract_ids(data) From 8496e7b9969f8924b971746ebc796821f3b8c1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 14:58:59 +0200 Subject: [PATCH 024/193] Undo new get --- tests/test_api.py | 2 +- trakt/tv.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index adea056b..d034e4ae 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,4 +12,4 @@ def test_api(): show = TVShow('Game of Thrones') assert show.title == 'Game of Thrones' - show.new_get() + print(show.new_get()) diff --git a/trakt/tv.py b/trakt/tv.py index 9a2b06ae..9c60d9f1 100644 --- a/trakt/tv.py +++ b/trakt/tv.py @@ -3,7 +3,6 @@ from collections import namedtuple from datetime import datetime, timedelta from trakt.core import Airs, Alias, Comment, Genre, delete, get -from trakt.decorators import get as new_get from trakt.errors import NotFoundException from trakt.sync import (Scrobbler, rate, comment, add_to_collection, add_to_watchlist, add_to_history, remove_from_history, @@ -242,11 +241,6 @@ def _get(self): data['airs'] = Airs(**data['airs']) self._build(data) - @new_get - def new_get(self): - data = yield self.ext_full - yield data - def _build(self, data): extract_ids(data) for key, val in data.items(): From 2844a96cf1aed0174f4f4ce58a9be4bd86d95ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 14:59:32 +0200 Subject: [PATCH 025/193] Use new decorators --- trakt/tv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trakt/tv.py b/trakt/tv.py index 9c60d9f1..806f4a25 100644 --- a/trakt/tv.py +++ b/trakt/tv.py @@ -2,7 +2,8 @@ """Interfaces to all of the TV objects offered by the Trakt.tv API""" from collections import namedtuple from datetime import datetime, timedelta -from trakt.core import Airs, Alias, Comment, Genre, delete, get +from trakt.core import Airs, Alias, Comment, Genre +from trakt.decorators import get, delete from trakt.errors import NotFoundException from trakt.sync import (Scrobbler, rate, comment, add_to_collection, add_to_watchlist, add_to_history, remove_from_history, From 387a4c6881958a673125be8a19583b2acdd17603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 15:00:56 +0200 Subject: [PATCH 026/193] Invoke api() only once --- trakt/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 51435872..cdc24f80 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -11,7 +11,7 @@ import sys import time from collections import namedtuple -from functools import wraps +from functools import wraps, lru_cache from requests_oauthlib import OAuth2Session from datetime import datetime, timedelta, timezone from trakt import errors @@ -633,7 +633,8 @@ def get_config(): ) -def get_api(): +@lru_cache(maxsize=None) +def api(): from trakt.api import HttpClient, TraktApi params = get_config() From ce526fa2f7fd115c7610107ddbd77a416275c8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 15:02:08 +0200 Subject: [PATCH 027/193] Export api --- trakt/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/core.py b/trakt/core.py index cdc24f80..60b15e25 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -20,7 +20,7 @@ __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'get', 'delete', 'post', 'put', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', 'REDIRECT_URI', 'HEADERS', 'CONFIG_PATH', 'OAUTH_TOKEN', - 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', + 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', 'api', 'APPLICATION_ID', 'get_device_code', 'get_device_token'] #: The base url for the Trakt API. Can be modified to run against different From 54abe3b25907f5d26c57217406d85071e57109a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 15:02:20 +0200 Subject: [PATCH 028/193] Use api() from core --- trakt/decorators.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/trakt/decorators.py b/trakt/decorators.py index b6335e5f..8b8e8f8d 100644 --- a/trakt/decorators.py +++ b/trakt/decorators.py @@ -5,6 +5,8 @@ from functools import wraps +from trakt.core import api + def _get_first(f, *args, **kwargs): """Extract the first value from the provided generator function *f* @@ -27,11 +29,6 @@ def _get_first(f, *args, **kwargs): return uri, generator, None -def api(): - from trakt.core import get_api - return get_api() - - def get(f): """Perform a HTTP GET request using the provided uri yielded from the *f* co-routine. The processed JSON results are then sent back to the From 7b4070a9ca14559174155adaee915916d49274f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 15:03:59 +0200 Subject: [PATCH 029/193] Cleanup test --- tests/test_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index d034e4ae..24118468 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- """trakt.tv functional tests""" from trakt.api import TraktApi -from trakt.core import get_api +from trakt.core import api from trakt.tv import TVShow def test_api(): - api = get_api() - assert isinstance(api, TraktApi) + api1 = api() + api2 = api() + assert isinstance(api1, TraktApi) + assert api1 == api2 show = TVShow('Game of Thrones') assert show.title == 'Game of Thrones' - - print(show.new_get()) From daac127c5255b4833be83e2f530ecbad2b4919a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 15:10:09 +0200 Subject: [PATCH 030/193] more tests --- tests/test_api.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 24118468..f7d2630e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """trakt.tv functional tests""" from trakt.api import TraktApi -from trakt.core import api +from trakt.core import api, Alias from trakt.tv import TVShow @@ -13,3 +13,8 @@ def test_api(): show = TVShow('Game of Thrones') assert show.title == 'Game of Thrones' + + assert len(show.aliases) == 305 + alias = show.aliases[0] + assert isinstance(alias, Alias) + assert alias.title == '冰與火之歌:權力遊戲' \ No newline at end of file From 1f3af816be89552831669ed847142b46745f6160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 15:16:06 +0200 Subject: [PATCH 031/193] fixup! Use new decorators --- trakt/calendar.py | 2 +- trakt/movies.py | 3 ++- trakt/people.py | 2 +- trakt/sync.py | 2 +- trakt/users.py | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/trakt/calendar.py b/trakt/calendar.py index 5fdae2d0..3543a741 100644 --- a/trakt/calendar.py +++ b/trakt/calendar.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Interfaces to all of the Calendar objects offered by the Trakt.tv API""" from pprint import pformat -from trakt.core import get +from trakt.decorators import get from trakt.movies import Movie from trakt.tv import TVEpisode, TVShow from trakt.utils import extract_ids, now, airs_date diff --git a/trakt/movies.py b/trakt/movies.py index fd5f15cf..2f9feb1b 100644 --- a/trakt/movies.py +++ b/trakt/movies.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """Interfaces to all of the Movie objects offered by the Trakt.tv API""" from collections import namedtuple -from trakt.core import Alias, Comment, Genre, get, delete +from trakt.core import Alias, Comment, Genre +from trakt.decorators import get, delete from trakt.sync import (Scrobbler, comment, rate, add_to_history, remove_from_history, add_to_watchlist, remove_from_watchlist, add_to_collection, diff --git a/trakt/people.py b/trakt/people.py index 074dfdb5..5ee8779e 100644 --- a/trakt/people.py +++ b/trakt/people.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Interfaces to all of the People objects offered by the Trakt.tv API""" -from trakt.core import get +from trakt.decorators import get from trakt.sync import search from trakt.utils import extract_ids, slugify diff --git a/trakt/sync.py b/trakt/sync.py index c4bf7cfe..689ca644 100644 --- a/trakt/sync.py +++ b/trakt/sync.py @@ -2,7 +2,7 @@ """This module contains Trakt.tv sync endpoint support functions""" from datetime import datetime, timezone -from trakt.core import get, post, delete +from trakt.decorators import get, post, delete from trakt.utils import slugify, extract_ids, timestamp diff --git a/trakt/users.py b/trakt/users.py index 523b83d3..b2343ef4 100644 --- a/trakt/users.py +++ b/trakt/users.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Interfaces to all of the User objects offered by the Trakt.tv API""" from collections import namedtuple -from trakt.core import get, post, delete +from trakt.decorators import get, delete, post from trakt.movies import Movie from trakt.people import Person from trakt.tv import TVShow, TVSeason, TVEpisode From 034570c0d6443235b9218d626edb9a6ea78300d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:18:27 +0200 Subject: [PATCH 032/193] Add TokenAuth class for requests https://docs.python-requests.org/en/latest/user/advanced/#custom-authentication --- trakt/api.py | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/trakt/api.py b/trakt/api.py index bc67ed79..5b43471f 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -7,6 +7,7 @@ from functools import lru_cache, wraps from typing import NamedTuple, List, Optional from urllib.parse import urljoin +from requests.auth import AuthBase from trakt import errors from requests import Session @@ -97,6 +98,123 @@ def error_map(self): return {err.http_code: err for err in errs} +class TokenAuth(dict, AuthBase): + """Attaches Trakt.tv token Authentication to the given Request object.""" + + def __init__(self, client: HttpClient, params: TraktApiParameters): + super().__init__() + self.client = client + self.CONFIG_PATH = None + self.update(**params._asdict()) + self.logger = logging.getLogger('trakt.api.oauth') + + def __call__(self, r): + [client_id, client_token] = self.get_token() + + r.headers.update({ + 'trakt-api-key': client_id, + 'Authorization': f'Bearer {client_token}', + }) + return r + + def get_token(self): + """Return client_id, client_token pair needed for Trakt.tv authentication + """ + + self.load_config() + # Check token validity and refresh token if needed + if (not self['OAUTH_TOKEN_VALID'] and self['OAUTH_EXPIRES_AT'] is not None + and self['OAUTH_REFRESH'] is not None): + self.validate_token() + # For backwards compatibility with trakt<=2.3.0 + # if api_key is not None and OAUTH_TOKEN is None: + # OAUTH_TOKEN = api_key + + return [ + self['CLIENT_ID'], + self['OAUTH_TOKEN'], + ] + + def validate_token(self): + """Check if current OAuth token has not expired""" + + current = datetime.now(tz=timezone.utc) + expires_at = datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) + if expires_at - current > timedelta(days=2): + self['OAUTH_TOKEN_VALID'] = True + else: + self.refresh_token() + + def refresh_token(self): + """Request Trakt API for a new valid OAuth token using refresh_token""" + + self.logger.info("OAuth token has expired, refreshing now...") + url = urljoin(self['BASE_URL'], '/oauth/token') + data = { + 'client_id': self['CLIENT_ID'], + 'client_secret': self['CLIENT_SECRET'], + 'refresh_token': self['OAUTH_REFRESH'], + 'redirect_uri': self['REDIRECT_URI'], + 'grant_type': 'refresh_token' + } + + try: + response = self.client.post(url, data) + except OAuthException: + self.logger.debug( + "Rejected - Unable to refresh expired OAuth token, " + "refresh_token is invalid" + ) + return + + self['OAUTH_TOKEN'] = response.get("access_token") + self['OAUTH_REFRESH'] = response.get("refresh_token") + self['OAUTH_EXPIRES_AT'] = response.get("created_at") + response.get("expires_in") + self['OAUTH_TOKEN_VALID'] = True + + self.logger.info( + "OAuth token successfully refreshed, valid until {}".format( + datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) + ) + ) + self.store_token( + CLIENT_ID=self['CLIENT_ID'], CLIENT_SECRET=self['CLIENT_SECRET'], + OAUTH_TOKEN=self['OAUTH_TOKEN'], OAUTH_REFRESH=self['OAUTH_REFRESH'], + OAUTH_EXPIRES_AT=self['OAUTH_EXPIRES_AT'], + ) + + def store_token(self, **kwargs): + """Helper function used to store Trakt configurations at ``CONFIG_PATH`` + + :param kwargs: Keyword args to store at ``CONFIG_PATH`` + """ + with open(self.CONFIG_PATH, 'w') as config_file: + json.dump(kwargs, config_file) + + def load_config(self): + """Manually load config from json config file.""" + # global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_EXPIRES_AT + # global OAUTH_REFRESH, APPLICATION_ID, CONFIG_PATH + if (self['CLIENT_ID'] is None or self['CLIENT_SECRET'] is None) and \ + os.path.exists(self.CONFIG_PATH): + # Load in trakt API auth data from CONFIG_PATH + with open(self.CONFIG_PATH) as config_file: + config_data = json.load(config_file) + + if self['CLIENT_ID'] is None: + self['CLIENT_ID'] = config_data.get('CLIENT_ID', None) + if self['CLIENT_SECRET'] is None: + self['CLIENT_SECRET'] = config_data.get('CLIENT_SECRET', None) + if self['OAUTH_TOKEN'] is None: + self['OAUTH_TOKEN'] = config_data.get('OAUTH_TOKEN', None) + if self['OAUTH_EXPIRES_AT'] is None: + self['OAUTH_EXPIRES_AT'] = config_data.get('OAUTH_EXPIRES_AT', None) + if self['OAUTH_REFRESH'] is None: + self['OAUTH_REFRESH'] = config_data.get('OAUTH_REFRESH', None) + if self['APPLICATION_ID'] is None: + self['APPLICATION_ID'] = config_data.get('APPLICATION_ID', None) + + class TraktApiTokenAuth(dict): """Class dealing with loading and updating oauth refresh token. """ From 2185902aa5155181e5692d832d48cad4c43cbbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:24:36 +0200 Subject: [PATCH 033/193] Implement token as Request auth plugin --- trakt/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 5b43471f..bb197e7b 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -34,6 +34,7 @@ class HttpClient: """ def __init__(self, base_url: str, session: Session): + self.auth = None self.base_url = base_url self.session = session self.logger = logging.getLogger('trakt.http_client') @@ -54,6 +55,9 @@ def put(self, url: str, data): def set_headers(self, headers): self.headers.update(headers) + def set_auth(self, auth): + self.auth = auth + def request(self, method, url, data=None): """Handle actually talking out to the trakt API, logging out debug information, raising any relevant `TraktException` Exception types, @@ -71,9 +75,9 @@ def request(self, method, url, data=None): self.logger.debug('%s: %s', method, url) self.logger.debug('method, url :: %s, %s', method, url) if method == 'get': # GETs need to pass data as params, not body - response = self.session.request(method, url, headers=self.headers, params=data) + response = self.session.request(method, url, headers=self.headers, auth=self.auth, params=data) else: - response = self.session.request(method, url, headers=self.headers, data=json.dumps(data)) + response = self.session.request(method, url, headers=self.headers, auth=self.auth, data=json.dumps(data)) self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) if response.status_code == 204: # HTTP no content return None From 3db02146e113cc9693a7a3ef34bce669d3f7ac85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:24:46 +0200 Subject: [PATCH 034/193] Update api() factory --- trakt/core.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 60b15e25..44315ea3 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -635,13 +635,15 @@ def get_config(): @lru_cache(maxsize=None) def api(): - from trakt.api import HttpClient, TraktApi + from trakt.api import HttpClient + from trakt.api import TokenAuth params = get_config() client = HttpClient(BASE_URL, session) - api = TraktApi(client, params) + auth = TokenAuth(client=client, params=params) + client.set_auth(auth) - return api + return client # Here we can simplify the code in each module by exporting these instance # method decorators as if they were simple functions. From fb82bb861c887128f6d63183173b0dadaf2fb117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:24:55 +0200 Subject: [PATCH 035/193] Update test_api --- tests/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index f7d2630e..04d2da04 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """trakt.tv functional tests""" -from trakt.api import TraktApi +from trakt.api import HttpClient from trakt.core import api, Alias from trakt.tv import TVShow @@ -8,7 +8,7 @@ def test_api(): api1 = api() api2 = api() - assert isinstance(api1, TraktApi) + assert isinstance(api1, HttpClient) assert api1 == api2 show = TVShow('Game of Thrones') From bff202d60aa17b78224a61eeb01ab4d926243b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:29:20 +0200 Subject: [PATCH 036/193] Delete unused TraktApiTokenAuth, TraktApi classes --- trakt/api.py | 149 --------------------------------------------------- 1 file changed, 149 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index bb197e7b..8200d4b0 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -217,152 +217,3 @@ def load_config(self): self['OAUTH_REFRESH'] = config_data.get('OAUTH_REFRESH', None) if self['APPLICATION_ID'] is None: self['APPLICATION_ID'] = config_data.get('APPLICATION_ID', None) - - -class TraktApiTokenAuth(dict): - """Class dealing with loading and updating oauth refresh token. - """ - - def __init__(self, client: HttpClient, params: TraktApiParameters): - super().__init__() - self.client = client - self.CONFIG_PATH = None - self.update(**params._asdict()) - self.logger = logging.getLogger('trakt.api.oauth') - - def get_token(self): - """Return client_id, client_token pair needed for Trakt.tv authentication - """ - - self.load_config() - # Check token validity and refresh token if needed - if (not self['OAUTH_TOKEN_VALID'] and self['OAUTH_EXPIRES_AT'] is not None - and self['OAUTH_REFRESH'] is not None): - self.validate_token() - # For backwards compatibility with trakt<=2.3.0 - # if api_key is not None and OAUTH_TOKEN is None: - # OAUTH_TOKEN = api_key - - return [ - self['CLIENT_ID'], - self['OAUTH_TOKEN'], - ] - - def validate_token(self): - """Check if current OAuth token has not expired""" - - current = datetime.now(tz=timezone.utc) - expires_at = datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) - if expires_at - current > timedelta(days=2): - self['OAUTH_TOKEN_VALID'] = True - else: - self.refresh_token() - - def refresh_token(self): - """Request Trakt API for a new valid OAuth token using refresh_token""" - - self.logger.info("OAuth token has expired, refreshing now...") - url = urljoin(self['BASE_URL'], '/oauth/token') - data = { - 'client_id': self['CLIENT_ID'], - 'client_secret': self['CLIENT_SECRET'], - 'refresh_token': self['OAUTH_REFRESH'], - 'redirect_uri': self['REDIRECT_URI'], - 'grant_type': 'refresh_token' - } - - try: - response = self.client.post(url, data) - except OAuthException: - self.logger.debug( - "Rejected - Unable to refresh expired OAuth token, " - "refresh_token is invalid" - ) - return - - self['OAUTH_TOKEN'] = response.get("access_token") - self['OAUTH_REFRESH'] = response.get("refresh_token") - self['OAUTH_EXPIRES_AT'] = response.get("created_at") + response.get("expires_in") - self['OAUTH_TOKEN_VALID'] = True - - self.logger.info( - "OAuth token successfully refreshed, valid until {}".format( - datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) - ) - ) - self.store_token( - CLIENT_ID=self['CLIENT_ID'], CLIENT_SECRET=self['CLIENT_SECRET'], - OAUTH_TOKEN=self['OAUTH_TOKEN'], OAUTH_REFRESH=self['OAUTH_REFRESH'], - OAUTH_EXPIRES_AT=self['OAUTH_EXPIRES_AT'], - ) - - def store_token(self, **kwargs): - """Helper function used to store Trakt configurations at ``CONFIG_PATH`` - - :param kwargs: Keyword args to store at ``CONFIG_PATH`` - """ - with open(self.CONFIG_PATH, 'w') as config_file: - json.dump(kwargs, config_file) - - def load_config(self): - """Manually load config from json config file.""" - # global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_EXPIRES_AT - # global OAUTH_REFRESH, APPLICATION_ID, CONFIG_PATH - if (self['CLIENT_ID'] is None or self['CLIENT_SECRET'] is None) and \ - os.path.exists(self.CONFIG_PATH): - # Load in trakt API auth data from CONFIG_PATH - with open(self.CONFIG_PATH) as config_file: - config_data = json.load(config_file) - - if self['CLIENT_ID'] is None: - self['CLIENT_ID'] = config_data.get('CLIENT_ID', None) - if self['CLIENT_SECRET'] is None: - self['CLIENT_SECRET'] = config_data.get('CLIENT_SECRET', None) - if self['OAUTH_TOKEN'] is None: - self['OAUTH_TOKEN'] = config_data.get('OAUTH_TOKEN', None) - if self['OAUTH_EXPIRES_AT'] is None: - self['OAUTH_EXPIRES_AT'] = config_data.get('OAUTH_EXPIRES_AT', None) - if self['OAUTH_REFRESH'] is None: - self['OAUTH_REFRESH'] = config_data.get('OAUTH_REFRESH', None) - if self['APPLICATION_ID'] is None: - self['APPLICATION_ID'] = config_data.get('APPLICATION_ID', None) - - -class TraktApi: - """This class contains all of the functionality required for interfacing - with the Trakt.tv API - """ - - def __init__(self, client: HttpClient, params: TraktApiParameters): - self.client = client - self.token_auth = TraktApiTokenAuth(client=self.client, params=params) - self.logger = logging.getLogger('trakt.api') - - def get(self, url: str): - self.authorize() - return self.client.get(url) - - def delete(self, url: str): - self.authorize() - self.client.delete(url) - - def post(self, url: str, data): - self.authorize() - return self.client.post(url, data=data) - - def put(self, url: str, data): - self.authorize() - return self.client.put(url, data=data) - - @lru_cache(maxsize=None) - def authorize(self): - [client_id, client_token] = self.token_auth.get_token() - - headers = { - 'trakt-api-key': client_id, - 'Authorization': f'Bearer {client_token}', - } - self.logger.debug('headers: %s', str(headers)) - self.client.set_headers(headers) - - return headers From c4f45160f4ee7abd896de9f44f3ac766b60a8dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:35:43 +0200 Subject: [PATCH 037/193] Update test to test for something that is mocked as well --- tests/test_api.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 04d2da04..91c784e7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,8 +13,4 @@ def test_api(): show = TVShow('Game of Thrones') assert show.title == 'Game of Thrones' - - assert len(show.aliases) == 305 - alias = show.aliases[0] - assert isinstance(alias, Alias) - assert alias.title == '冰與火之歌:權力遊戲' \ No newline at end of file + assert show.certification == 'TV-MA' \ No newline at end of file From 9a9f729ca6dccb95e6c9c23a5f42af374b01437a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:35:56 +0200 Subject: [PATCH 038/193] Update mock to use new api client --- tests/conftest.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 16129eeb..c1c9a09d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,12 @@ import json import os from copy import deepcopy +from functools import lru_cache + +from requests import Session import trakt +from trakt.api import HttpClient TESTS_DIR = os.path.dirname(__file__) MOCK_DATA_DIR = os.path.join(TESTS_DIR, "mock_data") @@ -25,31 +29,29 @@ ] -class MockCore(trakt.core.Core): - def __init__(self, *args, **kwargs): - super(MockCore, self).__init__(*args, **kwargs) +class MockCore(): + def __init__(self): self.mock_data = {} for mock_file in MOCK_DATA_FILES: with open(mock_file, encoding='utf-8') as f: self.mock_data.update(json.load(f)) - def _handle_request(self, method, url, data=None): - uri = url[len(trakt.core.BASE_URL):] + def request(self, method, uri, data=None): if uri.startswith('/'): uri = uri[1:] # use a deepcopy of the mocked data to ensure clean responses on every # request. this prevents rewrites to JSON responses from persisting method_responses = deepcopy(self.mock_data).get(uri, {}) - return method_responses.get(method.upper()) + response = method_responses.get(method.upper()) + if response is None: + print(f"No mock for {uri}") + return response -"""Override utility functions from trakt.core to use an underlying MockCore -instance +"""Override request function with MockCore instance """ -trakt.core.CORE = MockCore() -trakt.core.get = trakt.core.CORE.get -trakt.core.post = trakt.core.CORE.post -trakt.core.delete = trakt.core.CORE.delete -trakt.core.put = trakt.core.CORE.put + +trakt.core.api().request = MockCore().request + trakt.core.CLIENT_ID = 'FOO' trakt.core.CLIENT_SECRET = 'BAR' From b46fa05a33d8d236a99b160740bbe769195fe4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:36:24 +0200 Subject: [PATCH 039/193] Drop old decorators export --- trakt/core.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 44315ea3..f7c177aa 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -17,7 +17,7 @@ from trakt import errors __author__ = 'Jon Nappi' -__all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'get', 'delete', 'post', 'put', +__all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', 'REDIRECT_URI', 'HEADERS', 'CONFIG_PATH', 'OAUTH_TOKEN', 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', 'api', @@ -644,11 +644,3 @@ def api(): client.set_auth(auth) return client - -# Here we can simplify the code in each module by exporting these instance -# method decorators as if they were simple functions. -CORE = Core() -get = CORE.get -post = CORE.post -delete = CORE.delete -put = CORE.put \ No newline at end of file From 843f91df1aa2fad49a0379e55a1b8832e687e295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:37:05 +0200 Subject: [PATCH 040/193] Drop Core class --- trakt/core.py | 172 -------------------------------------------------- 1 file changed, 172 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index f7c177aa..abf5727c 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -442,178 +442,6 @@ def load_config(): APPLICATION_ID = config_data.get('APPLICATION_ID', None) -class Core(object): - """This class contains all of the functionality required for interfacing - with the Trakt.tv API - """ - - def __init__(self): - """Create a :class:`Core` instance and give it a logger attribute""" - self.logger = logging.getLogger('trakt.core') - - # Get all of our exceptions except the base exception - errs = [getattr(errors, att) for att in errors.__all__ - if att != 'TraktException'] - - # Map HTTP response codes to exception types - self.error_map = {err.http_code: err for err in errs} - self._bootstrapped = False - - def _bootstrap(self): - """Bootstrap your authentication environment when authentication is - needed and if a file at `CONFIG_PATH` exists. - The process is completed by setting the client id header. - """ - - if self._bootstrapped: - return - self._bootstrapped = True - - global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT - global OAUTH_REFRESH, OAUTH_TOKEN - - load_config() - # Check token validity and refresh token if needed - if (not OAUTH_TOKEN_VALID and OAUTH_EXPIRES_AT is not None - and OAUTH_REFRESH is not None): - _validate_token(self) - # For backwards compatibility with trakt<=2.3.0 - if api_key is not None and OAUTH_TOKEN is None: - OAUTH_TOKEN = api_key - - @staticmethod - def _get_first(f, *args, **kwargs): - """Extract the first value from the provided generator function *f* - - :param f: A generator function to extract data from - :param args: Non keyword args for the generator function - :param kwargs: Keyword args for the generator function - :return: The full url for the resource, a generator, and either a data - payload or `None` - """ - generator = f(*args, **kwargs) - uri = next(generator) - if not isinstance(uri, (str, tuple)): - # Allow properties to safely yield arbitrary data - return uri - if isinstance(uri, tuple): - uri, data = uri - return BASE_URL + uri, generator, data - else: - return BASE_URL + uri, generator, None - - def _handle_request(self, method, url, data=None): - """Handle actually talking out to the trakt API, logging out debug - information, raising any relevant `TraktException` Exception types, - and extracting and returning JSON data - - :param method: The HTTP method we're executing on. Will be one of - post, put, delete, get - :param url: The fully qualified url to send our request to - :param data: Optional data payload to send to the API - :return: The decoded JSON response from the Trakt API - :raises TraktException: If any non-200 return code is encountered - """ - self.logger.debug('%s: %s', method, url) - HEADERS['trakt-api-key'] = CLIENT_ID - HEADERS['Authorization'] = 'Bearer {0}'.format(OAUTH_TOKEN) - self.logger.debug('headers: %s', str(HEADERS)) - self.logger.debug('method, url :: %s, %s', method, url) - if method == 'get': # GETs need to pass data as params, not body - response = session.request(method, url, headers=HEADERS, - params=data) - else: - response = session.request(method, url, headers=HEADERS, - data=json.dumps(data)) - self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) - if response.status_code in self.error_map: - raise self.error_map[response.status_code](response) - elif response.status_code == 204: # HTTP no content - return None - json_data = json.loads(response.content.decode('UTF-8', 'ignore')) - return json_data - - def get(self, f): - """Perform a HTTP GET request using the provided uri yielded from the - *f* co-routine. The processed JSON results are then sent back to the - co-routine for post-processing, the results of which are then returned - - :param f: Generator co-routine that yields uri, args, and processed - results - :return: The results of the generator co-routine - """ - @wraps(f) - def inner(*args, **kwargs): - self._bootstrap() - resp = self._get_first(f, *args, **kwargs) - if not isinstance(resp, tuple): - # Handle cached property responses - return resp - url, generator, _ = resp - json_data = self._handle_request('get', url) - try: - return generator.send(json_data) - except StopIteration: - return None - return inner - - def delete(self, f): - """Perform an HTTP DELETE request using the provided uri - - :param f: Function that returns a uri to delete to - """ - @wraps(f) - def inner(*args, **kwargs): - self._bootstrap() - generator = f(*args, **kwargs) - uri = next(generator) - url = BASE_URL + uri - self._handle_request('delete', url) - return inner - - def post(self, f): - """Perform an HTTP POST request using the provided uri and optional - args yielded from the *f* co-routine. The processed JSON results are - then sent back to the co-routine for post-processing, the results of - which are then returned - - :param f: Generator co-routine that yields uri, args, and processed - results - :return: The results of the generator co-routine - """ - @wraps(f) - def inner(*args, **kwargs): - self._bootstrap() - url, generator, args = self._get_first(f, *args, **kwargs) - json_data = self._handle_request('post', url, data=args) - try: - return generator.send(json_data) - except StopIteration: - return None - return inner - - def put(self, f): - """Perform an HTTP PUT request using the provided uri and optional args - yielded from the *f* co-routine. The processed JSON results are then - sent back to the co-routine for post-processing, the results of which - are then returned - - :param f: Generator co-routine that yields uri, args, and processed - results - :return: The results of the generator co-routine - """ - @wraps(f) - def inner(*args, **kwargs): - self._bootstrap() - url, generator, args = self._get_first(f, *args, **kwargs) - json_data = self._handle_request('put', url, data=args) - try: - return generator.send(json_data) - except StopIteration: - return None - return inner - - def get_config(): global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT global OAUTH_REFRESH, OAUTH_TOKEN From a6c738159c542e03b64521d535842b8a5a7d261c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:38:43 +0200 Subject: [PATCH 041/193] Drop unused headers --- trakt/api.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 8200d4b0..810851ee 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -38,7 +38,6 @@ def __init__(self, base_url: str, session: Session): self.base_url = base_url self.session = session self.logger = logging.getLogger('trakt.http_client') - self.headers = {} def get(self, url: str): return self.request('get', url) @@ -52,9 +51,6 @@ def post(self, url: str, data): def put(self, url: str, data): return self.request('put', url, data=data) - def set_headers(self, headers): - self.headers.update(headers) - def set_auth(self, auth): self.auth = auth @@ -75,9 +71,9 @@ def request(self, method, url, data=None): self.logger.debug('%s: %s', method, url) self.logger.debug('method, url :: %s, %s', method, url) if method == 'get': # GETs need to pass data as params, not body - response = self.session.request(method, url, headers=self.headers, auth=self.auth, params=data) + response = self.session.request(method, url, auth=self.auth, params=data) else: - response = self.session.request(method, url, headers=self.headers, auth=self.auth, data=json.dumps(data)) + response = self.session.request(method, url, auth=self.auth, data=json.dumps(data)) self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) if response.status_code == 204: # HTTP no content return None From 56477ee729464a57f561cff1d2430a444c330ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:39:17 +0200 Subject: [PATCH 042/193] Cleanup --- trakt/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 810851ee..3167a700 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -193,8 +193,7 @@ def store_token(self, **kwargs): def load_config(self): """Manually load config from json config file.""" - # global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_EXPIRES_AT - # global OAUTH_REFRESH, APPLICATION_ID, CONFIG_PATH + if (self['CLIENT_ID'] is None or self['CLIENT_SECRET'] is None) and \ os.path.exists(self.CONFIG_PATH): # Load in trakt API auth data from CONFIG_PATH From c440ead704e0a736f7298c59642b09dd94e9245c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:40:16 +0200 Subject: [PATCH 043/193] Invert to exit early --- trakt/api.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 3167a700..6d9b6ab6 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -194,21 +194,22 @@ def store_token(self, **kwargs): def load_config(self): """Manually load config from json config file.""" - if (self['CLIENT_ID'] is None or self['CLIENT_SECRET'] is None) and \ - os.path.exists(self.CONFIG_PATH): - # Load in trakt API auth data from CONFIG_PATH - with open(self.CONFIG_PATH) as config_file: - config_data = json.load(config_file) - - if self['CLIENT_ID'] is None: - self['CLIENT_ID'] = config_data.get('CLIENT_ID', None) - if self['CLIENT_SECRET'] is None: - self['CLIENT_SECRET'] = config_data.get('CLIENT_SECRET', None) - if self['OAUTH_TOKEN'] is None: - self['OAUTH_TOKEN'] = config_data.get('OAUTH_TOKEN', None) - if self['OAUTH_EXPIRES_AT'] is None: - self['OAUTH_EXPIRES_AT'] = config_data.get('OAUTH_EXPIRES_AT', None) - if self['OAUTH_REFRESH'] is None: - self['OAUTH_REFRESH'] = config_data.get('OAUTH_REFRESH', None) - if self['APPLICATION_ID'] is None: - self['APPLICATION_ID'] = config_data.get('APPLICATION_ID', None) + if self['CLIENT_ID'] is not None and self['CLIENT_SECRET'] is not None or not os.path.exists(self.CONFIG_PATH): + return + + # Load in trakt API auth data from CONFIG_PATH + with open(self.CONFIG_PATH) as config_file: + config_data = json.load(config_file) + + if self['CLIENT_ID'] is None: + self['CLIENT_ID'] = config_data.get('CLIENT_ID', None) + if self['CLIENT_SECRET'] is None: + self['CLIENT_SECRET'] = config_data.get('CLIENT_SECRET', None) + if self['OAUTH_TOKEN'] is None: + self['OAUTH_TOKEN'] = config_data.get('OAUTH_TOKEN', None) + if self['OAUTH_EXPIRES_AT'] is None: + self['OAUTH_EXPIRES_AT'] = config_data.get('OAUTH_EXPIRES_AT', None) + if self['OAUTH_REFRESH'] is None: + self['OAUTH_REFRESH'] = config_data.get('OAUTH_REFRESH', None) + if self['APPLICATION_ID'] is None: + self['APPLICATION_ID'] = config_data.get('APPLICATION_ID', None) From a29d398482e2ba41ecce66ebe3e59743c4b2c4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:42:15 +0200 Subject: [PATCH 044/193] Use loop rather massive typing --- trakt/api.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 6d9b6ab6..de264a53 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -201,15 +201,17 @@ def load_config(self): with open(self.CONFIG_PATH) as config_file: config_data = json.load(config_file) - if self['CLIENT_ID'] is None: - self['CLIENT_ID'] = config_data.get('CLIENT_ID', None) - if self['CLIENT_SECRET'] is None: - self['CLIENT_SECRET'] = config_data.get('CLIENT_SECRET', None) - if self['OAUTH_TOKEN'] is None: - self['OAUTH_TOKEN'] = config_data.get('OAUTH_TOKEN', None) - if self['OAUTH_EXPIRES_AT'] is None: - self['OAUTH_EXPIRES_AT'] = config_data.get('OAUTH_EXPIRES_AT', None) - if self['OAUTH_REFRESH'] is None: - self['OAUTH_REFRESH'] = config_data.get('OAUTH_REFRESH', None) - if self['APPLICATION_ID'] is None: - self['APPLICATION_ID'] = config_data.get('APPLICATION_ID', None) + keys = [ + 'APPLICATION_ID', + 'CLIENT_ID', + 'CLIENT_SECRET', + 'OAUTH_EXPIRES_AT', + 'OAUTH_REFRESH', + 'OAUTH_TOKEN', + ] + + for key in keys: + if self[key] is not None: + continue + + self[key] = config_data.get(key, None) From fb8297e47c2836c05226b7a9a72a22b5290d5345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:43:00 +0200 Subject: [PATCH 045/193] Cleanup --- trakt/api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index de264a53..f64258de 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -126,9 +126,6 @@ def get_token(self): if (not self['OAUTH_TOKEN_VALID'] and self['OAUTH_EXPIRES_AT'] is not None and self['OAUTH_REFRESH'] is not None): self.validate_token() - # For backwards compatibility with trakt<=2.3.0 - # if api_key is not None and OAUTH_TOKEN is None: - # OAUTH_TOKEN = api_key return [ self['CLIENT_ID'], From 8c498fe5d80549a5ab0ad3a86e7afef47865a2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:44:53 +0200 Subject: [PATCH 046/193] Deep copy response rather whole mock data --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c1c9a09d..c1aa6156 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,11 +41,11 @@ def request(self, method, uri, data=None): uri = uri[1:] # use a deepcopy of the mocked data to ensure clean responses on every # request. this prevents rewrites to JSON responses from persisting - method_responses = deepcopy(self.mock_data).get(uri, {}) + method_responses = self.mock_data.get(uri, {}) response = method_responses.get(method.upper()) if response is None: print(f"No mock for {uri}") - return response + return deepcopy(response) """Override request function with MockCore instance From acda749c9c60fe47ae7fc36f73d22b52bea5437b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:46:08 +0200 Subject: [PATCH 047/193] Revert "Drop unused headers" This reverts commit a6c738159c542e03b64521d535842b8a5a7d261c. --- trakt/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index f64258de..270569a8 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -38,6 +38,7 @@ def __init__(self, base_url: str, session: Session): self.base_url = base_url self.session = session self.logger = logging.getLogger('trakt.http_client') + self.headers = {} def get(self, url: str): return self.request('get', url) @@ -51,6 +52,9 @@ def post(self, url: str, data): def put(self, url: str, data): return self.request('put', url, data=data) + def set_headers(self, headers): + self.headers.update(headers) + def set_auth(self, auth): self.auth = auth @@ -71,9 +75,9 @@ def request(self, method, url, data=None): self.logger.debug('%s: %s', method, url) self.logger.debug('method, url :: %s, %s', method, url) if method == 'get': # GETs need to pass data as params, not body - response = self.session.request(method, url, auth=self.auth, params=data) + response = self.session.request(method, url, headers=self.headers, auth=self.auth, params=data) else: - response = self.session.request(method, url, auth=self.auth, data=json.dumps(data)) + response = self.session.request(method, url, headers=self.headers, auth=self.auth, data=json.dumps(data)) self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) if response.status_code == 204: # HTTP no content return None From 7f470d1067837f7321ca8f4e2264639ece617867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:46:47 +0200 Subject: [PATCH 048/193] Set default headers (inline them rather?) --- trakt/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trakt/core.py b/trakt/core.py index abf5727c..fa0f7d1b 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -468,6 +468,7 @@ def api(): params = get_config() client = HttpClient(BASE_URL, session) + client.set_headers(params.HEADERS) auth = TokenAuth(client=client, params=params) client.set_auth(auth) From d5b5e80ddc3051829f2e124f89846ff18d6e9221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:48:20 +0200 Subject: [PATCH 049/193] Set config path for token auth --- trakt/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 270569a8..0341f89e 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -105,10 +105,10 @@ def error_map(self): class TokenAuth(dict, AuthBase): """Attaches Trakt.tv token Authentication to the given Request object.""" - def __init__(self, client: HttpClient, params: TraktApiParameters): + def __init__(self, client: HttpClient, config_path: str, params: TraktApiParameters): super().__init__() self.client = client - self.CONFIG_PATH = None + self.config_path = config_path self.update(**params._asdict()) self.logger = logging.getLogger('trakt.api.oauth') @@ -189,17 +189,17 @@ def store_token(self, **kwargs): :param kwargs: Keyword args to store at ``CONFIG_PATH`` """ - with open(self.CONFIG_PATH, 'w') as config_file: + with open(self.config_path, 'w') as config_file: json.dump(kwargs, config_file) def load_config(self): """Manually load config from json config file.""" - if self['CLIENT_ID'] is not None and self['CLIENT_SECRET'] is not None or not os.path.exists(self.CONFIG_PATH): + if self['CLIENT_ID'] is not None and self['CLIENT_SECRET'] is not None or not os.path.exists(self.config_path): return # Load in trakt API auth data from CONFIG_PATH - with open(self.CONFIG_PATH) as config_file: + with open(self.config_path) as config_file: config_data = json.load(config_file) keys = [ From e4d7a6962b5a67b592b4309dec84ba688732b4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:48:25 +0200 Subject: [PATCH 050/193] Update factory --- trakt/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/core.py b/trakt/core.py index fa0f7d1b..bb9b9188 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -469,7 +469,7 @@ def api(): params = get_config() client = HttpClient(BASE_URL, session) client.set_headers(params.HEADERS) - auth = TokenAuth(client=client, params=params) + auth = TokenAuth(client=client, config_path=CONFIG_PATH, params=params) client.set_auth(auth) return client From 70f5076abddbaacf0e81a1243d5ac50eab477635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:49:48 +0200 Subject: [PATCH 051/193] No base url needed --- trakt/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 0341f89e..e7756dc3 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -150,7 +150,6 @@ def refresh_token(self): """Request Trakt API for a new valid OAuth token using refresh_token""" self.logger.info("OAuth token has expired, refreshing now...") - url = urljoin(self['BASE_URL'], '/oauth/token') data = { 'client_id': self['CLIENT_ID'], 'client_secret': self['CLIENT_SECRET'], @@ -160,7 +159,7 @@ def refresh_token(self): } try: - response = self.client.post(url, data) + response = self.client.post('/oauth/token', data) except OAuthException: self.logger.debug( "Rejected - Unable to refresh expired OAuth token, " From 89fc7e0d36a210552ced2a16b6b42d7e5481ddd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:50:19 +0200 Subject: [PATCH 052/193] Remove BASE_URL from TraktApiParameters --- trakt/api.py | 1 - trakt/core.py | 1 - 2 files changed, 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index e7756dc3..1b7688ba 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -18,7 +18,6 @@ class TraktApiParameters(NamedTuple): - BASE_URL: str CLIENT_ID: Optional[str] CLIENT_SECRET: Optional[str] OAUTH_EXPIRES_AT: Optional[int] diff --git a/trakt/core.py b/trakt/core.py index bb9b9188..164ef960 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -449,7 +449,6 @@ def get_config(): from trakt.api import TraktApiParameters return TraktApiParameters( - BASE_URL=BASE_URL, CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, From d30aa45120ab8b0641eb3859ced0648c567303be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 17:58:01 +0200 Subject: [PATCH 053/193] Drop unused Core helpers --- trakt/core.py | 75 --------------------------------------------------- 1 file changed, 75 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 164ef960..154c5812 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -367,81 +367,6 @@ def init(*args, **kwargs): 'updated_at', 'likes', 'user_rating']) - -def _validate_token(s): - """Check if current OAuth token has not expired""" - global OAUTH_TOKEN_VALID - current = datetime.now(tz=timezone.utc) - expires_at = datetime.fromtimestamp(OAUTH_EXPIRES_AT, tz=timezone.utc) - if expires_at - current > timedelta(days=2): - OAUTH_TOKEN_VALID = True - else: - _refresh_token(s) - - -def _refresh_token(s): - """Request Trakt API for a new valid OAuth token using refresh_token""" - global OAUTH_TOKEN, OAUTH_EXPIRES_AT, OAUTH_REFRESH, OAUTH_TOKEN_VALID - s.logger.info("OAuth token has expired, refreshing now...") - url = urljoin(BASE_URL, '/oauth/token') - data = { - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET, - 'refresh_token': OAUTH_REFRESH, - 'redirect_uri': REDIRECT_URI, - 'grant_type': 'refresh_token' - } - response = session.post(url, json=data, headers=HEADERS) - s.logger.debug('RESPONSE [post] (%s): %s', url, str(response)) - if response.status_code == 200: - data = response.json() - OAUTH_TOKEN = data.get("access_token") - OAUTH_REFRESH = data.get("refresh_token") - OAUTH_EXPIRES_AT = data.get("created_at") + data.get("expires_in") - OAUTH_TOKEN_VALID = True - s.logger.info( - "OAuth token successfully refreshed, valid until {}".format( - datetime.fromtimestamp(OAUTH_EXPIRES_AT, tz=timezone.utc) - ) - ) - _store( - CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, - OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT - ) - elif response.status_code == 401: - s.logger.debug( - "Rejected - Unable to refresh expired OAuth token, " - "refresh_token is invalid" - ) - elif response.status_code in s.error_map: - raise s.error_map[response.status_code](response) - - -def load_config(): - """Manually load config from json config file.""" - global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_EXPIRES_AT - global OAUTH_REFRESH, APPLICATION_ID, CONFIG_PATH - if (CLIENT_ID is None or CLIENT_SECRET is None) and \ - os.path.exists(CONFIG_PATH): - # Load in trakt API auth data from CONFIG_PATH - with open(CONFIG_PATH) as config_file: - config_data = json.load(config_file) - - if CLIENT_ID is None: - CLIENT_ID = config_data.get('CLIENT_ID', None) - if CLIENT_SECRET is None: - CLIENT_SECRET = config_data.get('CLIENT_SECRET', None) - if OAUTH_TOKEN is None: - OAUTH_TOKEN = config_data.get('OAUTH_TOKEN', None) - if OAUTH_EXPIRES_AT is None: - OAUTH_EXPIRES_AT = config_data.get('OAUTH_EXPIRES_AT', None) - if OAUTH_REFRESH is None: - OAUTH_REFRESH = config_data.get('OAUTH_REFRESH', None) - if APPLICATION_ID is None: - APPLICATION_ID = config_data.get('APPLICATION_ID', None) - - def get_config(): global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT global OAUTH_REFRESH, OAUTH_TOKEN From 94d36a1a1a8a446f72a7111a4841972f978c2112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:01:11 +0200 Subject: [PATCH 054/193] Add TraktApiParameters to core module --- trakt/core.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 154c5812..cf1c8054 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -5,6 +5,7 @@ import json import logging import os +from typing import NamedTuple, Optional from urllib.parse import urljoin import requests @@ -73,6 +74,16 @@ session = requests.Session() +class TraktApiParameters(NamedTuple): + CLIENT_ID: Optional[str] + CLIENT_SECRET: Optional[str] + OAUTH_EXPIRES_AT: Optional[int] + OAUTH_REFRESH: Optional[int] + OAUTH_TOKEN: Optional[str] + OAUTH_TOKEN_VALID: Optional[bool] + HEADERS: Optional[dict[str, str]] + + def _store(**kwargs): """Helper function used to store Trakt configurations at ``CONFIG_PATH`` @@ -231,9 +242,9 @@ def get_device_code(client_id=None, client_secret=None): json=data, headers=headers).json() print('Your user code is: {user_code}, please navigate to ' '{verification_url} to authenticate'.format( - user_code=device_response.get('user_code'), - verification_url=device_response.get('verification_url') - )) + user_code=device_response.get('user_code'), + verification_url=device_response.get('verification_url') + )) device_response['requested'] = time.time() return device_response @@ -372,7 +383,6 @@ def get_config(): global OAUTH_REFRESH, OAUTH_TOKEN global CLIENT_ID, CLIENT_SECRET - from trakt.api import TraktApiParameters return TraktApiParameters( CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, From 1d03ef731720ab75114054c3cd68c124340b95df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:01:19 +0200 Subject: [PATCH 055/193] Use TraktApiParameters from core module --- trakt/api.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 1b7688ba..49e7bb6d 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -14,20 +14,10 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' +from trakt.core import TraktApiParameters from trakt.errors import OAuthException -class TraktApiParameters(NamedTuple): - CLIENT_ID: Optional[str] - CLIENT_SECRET: Optional[str] - OAUTH_EXPIRES_AT: Optional[int] - OAUTH_REFRESH: Optional[int] - OAUTH_TOKEN: Optional[str] - OAUTH_TOKEN_VALID: Optional[bool] - REDIRECT_URI: str - HEADERS: Optional[dict[str, str]] - - class HttpClient: """Class for abstracting HTTP requests """ From c2f7c5c043c0ff45c4be963476faf888718f8c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:01:56 +0200 Subject: [PATCH 056/193] fixup! Add TraktApiParameters to core module --- trakt/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trakt/core.py b/trakt/core.py index cf1c8054..4042b397 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -81,6 +81,7 @@ class TraktApiParameters(NamedTuple): OAUTH_REFRESH: Optional[int] OAUTH_TOKEN: Optional[str] OAUTH_TOKEN_VALID: Optional[bool] + REDIRECT_URI: str HEADERS: Optional[dict[str, str]] From 451029058ea945da0aba75290ca3f291a939a4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:03:13 +0200 Subject: [PATCH 057/193] Rename to AuthConfig --- trakt/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 49e7bb6d..32046f5a 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -14,7 +14,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' -from trakt.core import TraktApiParameters +from trakt.core import AuthConfig from trakt.errors import OAuthException @@ -94,7 +94,7 @@ def error_map(self): class TokenAuth(dict, AuthBase): """Attaches Trakt.tv token Authentication to the given Request object.""" - def __init__(self, client: HttpClient, config_path: str, params: TraktApiParameters): + def __init__(self, client: HttpClient, config_path: str, params: AuthConfig): super().__init__() self.client = client self.config_path = config_path From bc6131008e3aeee3ebdc3621d138732aeb33c62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:03:19 +0200 Subject: [PATCH 058/193] Rename to AuthConfig --- trakt/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 4042b397..35c72cb1 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -74,15 +74,15 @@ session = requests.Session() -class TraktApiParameters(NamedTuple): +class AuthConfig(NamedTuple): CLIENT_ID: Optional[str] CLIENT_SECRET: Optional[str] OAUTH_EXPIRES_AT: Optional[int] OAUTH_REFRESH: Optional[int] OAUTH_TOKEN: Optional[str] OAUTH_TOKEN_VALID: Optional[bool] - REDIRECT_URI: str - HEADERS: Optional[dict[str, str]] + HEADERS: Optional[dict[str, str]] = HEADERS + REDIRECT_URI: str = REDIRECT_URI def _store(**kwargs): @@ -384,7 +384,7 @@ def get_config(): global OAUTH_REFRESH, OAUTH_TOKEN global CLIENT_ID, CLIENT_SECRET - return TraktApiParameters( + return AuthConfig( CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, From 6fe62e1da7fe6d2b455054bfda5ebe94e6146c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:06:11 +0200 Subject: [PATCH 059/193] Add auth module --- trakt/auth.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 trakt/auth.py diff --git a/trakt/auth.py b/trakt/auth.py new file mode 100644 index 00000000..5777c0f4 --- /dev/null +++ b/trakt/auth.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +"""Authentication methods""" + +__author__ = 'Jon Nappi, Elan Ruusamäe' From ddfec0db57a1db9da824763103c97f6da67fd429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:09:02 +0200 Subject: [PATCH 060/193] Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth_auth.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 trakt/auth/oauth_auth.py diff --git a/trakt/auth/oauth_auth.py b/trakt/auth/oauth_auth.py new file mode 100644 index 00000000..e69de29b From 87ab38ac276093b880807b6d87b36db5e0ca94a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:09:38 +0200 Subject: [PATCH 061/193] fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth_auth.py | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/trakt/auth/oauth_auth.py b/trakt/auth/oauth_auth.py index e69de29b..92f2b1e3 100644 --- a/trakt/auth/oauth_auth.py +++ b/trakt/auth/oauth_auth.py @@ -0,0 +1,46 @@ + +def oauth_auth(username, client_id=None, client_secret=None, store=False, + oauth_cb=_terminal_oauth_pin): + """Generate an access_token to allow your application to authenticate via + OAuth + + :param username: Your trakt.tv username + :param client_id: Your Trakt OAuth Application's Client ID + :param client_secret: Your Trakt OAuth Application's Client Secret + :param store: Boolean flag used to determine if your trakt api auth data + should be stored locally on the system. Default is :const:`False` for + the security conscious + :param oauth_cb: Callback function to handle the retrieving of the OAuth + PIN. Default function `_terminal_oauth_pin` for terminal auth + :return: Your OAuth access token + """ + global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN + if client_id is None and client_secret is None: + client_id, client_secret = _get_client_info() + CLIENT_ID, CLIENT_SECRET = client_id, client_secret + HEADERS['trakt-api-key'] = CLIENT_ID + + authorization_base_url = urljoin(BASE_URL, '/oauth/authorize') + token_url = urljoin(BASE_URL, '/oauth/token') + + # OAuth endpoints given in the API documentation + oauth = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI, state=None) + + # Authorization URL to redirect user to Trakt for authorization + authorization_url, _ = oauth.authorization_url(authorization_base_url, + username=username) + + # Calling callback function to get the OAuth PIN + oauth_pin = oauth_cb(authorization_url) + + # Fetch, assign, and return the access token + oauth.fetch_token(token_url, client_secret=CLIENT_SECRET, code=oauth_pin) + OAUTH_TOKEN = oauth.token['access_token'] + OAUTH_REFRESH = oauth.token['refresh_token'] + OAUTH_EXPIRES_AT = oauth.token["created_at"] + oauth.token["expires_in"] + + if store: + _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, + OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, + OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT) + return OAUTH_TOKEN From 7be10543a9c73af80e661fa48b4f84a9d4cd828d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:11:06 +0200 Subject: [PATCH 062/193] Add trakt/auth/pin_auth.py module --- trakt/auth/pin_auth.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 trakt/auth/pin_auth.py diff --git a/trakt/auth/pin_auth.py b/trakt/auth/pin_auth.py new file mode 100644 index 00000000..1635615d --- /dev/null +++ b/trakt/auth/pin_auth.py @@ -0,0 +1,42 @@ +def pin_auth(pin=None, client_id=None, client_secret=None, store=False): + """Generate an access_token from a Trakt API PIN code. + + :param pin: Optional Trakt API PIN code. If one is not specified, you will + be prompted to go generate one + :param client_id: The oauth client_id for authenticating to the trakt API. + :param client_secret: The oauth client_secret for authenticating to the + trakt API. + :param store: Boolean flag used to determine if your trakt api auth data + should be stored locally on the system. Default is :const:`False` for + the security conscious + :return: Your OAuth access token + """ + global OAUTH_TOKEN, CLIENT_ID, CLIENT_SECRET + CLIENT_ID, CLIENT_SECRET = client_id, client_secret + if client_id is None and client_secret is None: + CLIENT_ID, CLIENT_SECRET = _get_client_info(app_id=True) + if pin is None and APPLICATION_ID is None: + print('You must set the APPLICATION_ID of the Trakt application you ' + 'wish to use. You can find this ID by visiting the following ' + 'URL.') + print('https://trakt.tv/oauth/applications') + sys.exit(1) + if pin is None: + print('If you do not have a Trakt.tv PIN, please visit the following ' + 'url and log in to generate one.') + pin_url = 'https://trakt.tv/pin/{id}'.format(id=APPLICATION_ID) + print(pin_url) + pin = input('Please enter your PIN: ') + args = {'code': pin, + 'redirect_uri': REDIRECT_URI, + 'grant_type': 'authorization_code', + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET} + + response = session.post(''.join([BASE_URL, '/oauth/token']), data=args) + OAUTH_TOKEN = response.json().get('access_token', None) + + if store: + _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, + OAUTH_TOKEN=OAUTH_TOKEN, APPLICATION_ID=APPLICATION_ID) + return OAUTH_TOKEN From 0d5491935d082900cfe5a5521afafbb569cddf75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:11:38 +0200 Subject: [PATCH 063/193] fixup! fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth_auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/trakt/auth/oauth_auth.py b/trakt/auth/oauth_auth.py index 92f2b1e3..33be6684 100644 --- a/trakt/auth/oauth_auth.py +++ b/trakt/auth/oauth_auth.py @@ -1,4 +1,3 @@ - def oauth_auth(username, client_id=None, client_secret=None, store=False, oauth_cb=_terminal_oauth_pin): """Generate an access_token to allow your application to authenticate via From 7033e4137a540016f47f011e2878d92d1e61eb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:12:22 +0200 Subject: [PATCH 064/193] Add trakt/auth/device_auth.py module --- trakt/auth/device_auth.py | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 trakt/auth/device_auth.py diff --git a/trakt/auth/device_auth.py b/trakt/auth/device_auth.py new file mode 100644 index 00000000..d1f727bd --- /dev/null +++ b/trakt/auth/device_auth.py @@ -0,0 +1,58 @@ +def device_auth(client_id=None, client_secret=None, store=False): + """Process for authenticating using device authentication. + + The function will attempt getting the device_id, and provide + the user with a url and code. After getting the device + id, a timer is started to poll periodic for a successful authentication. + This is a blocking action, meaning you + will not be able to run any other code, while waiting for an access token. + + If you want more control over the authentication flow, use the functions + get_device_code and get_device_token. + Where poll_for_device_token will check if the "offline" + authentication was successful. + + :param client_id: Your Trakt OAuth Application's Client ID + :param client_secret: Your Trakt OAuth Application's Client Secret + :param store: Boolean flag used to determine if your trakt api auth data + should be stored locally on the system. Default is :const:`False` for + the security conscious + :return: A dict with the authentication result. + Or False of authentication failed. + """ + error_messages = { + 404: 'Invalid device_code', + 409: 'You already approved this code', + 410: 'The tokens have expired, restart the process', + 418: 'You explicitly denied this code', + } + + success_message = ( + "You've been successfully authenticated. " + "With access_token {access_token} and refresh_token {refresh_token}" + ) + + response = get_device_code(client_id=client_id, + client_secret=client_secret) + device_code = response['device_code'] + interval = response['interval'] + + # No need to check for expiration, the API will notify us. + while True: + response = get_device_token(device_code, client_id, client_secret, + store) + + if response.status_code == 200: + print(success_message.format_map(response.json())) + break + + elif response.status_code == 429: # slow down + interval *= 2 + + elif response.status_code != 400: # not pending + print(error_messages.get(response.status_code, response.reason)) + break + + time.sleep(interval) + + return response From 0f7a9490f6ea76e41a05c779b77f6eabdfb5c51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:15:16 +0200 Subject: [PATCH 065/193] Add auth init() --- trakt/auth.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/trakt/auth.py b/trakt/auth.py index 5777c0f4..bbf42b91 100644 --- a/trakt/auth.py +++ b/trakt/auth.py @@ -2,3 +2,26 @@ """Authentication methods""" __author__ = 'Jon Nappi, Elan Ruusamäe' + +from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH + + +def pin_auth(): + pass + + +def oauth_auth(): + pass + + +def device_auth(): + pass + + +def init(AUTH_METHOD, *args, **kwargs): + auth_method = { + PIN_AUTH: pin_auth, OAUTH_AUTH: oauth_auth, DEVICE_AUTH: device_auth + } + + """Run the auth function specified by *AUTH_METHOD*""" + return auth_method.get(AUTH_METHOD, PIN_AUTH)(*args, **kwargs) From 22952afeffbd0f2cdac178befe37eb0e47a6fa30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:15:38 +0200 Subject: [PATCH 066/193] fixup! Add auth init() --- trakt/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/auth.py b/trakt/auth.py index bbf42b91..9ccb09ec 100644 --- a/trakt/auth.py +++ b/trakt/auth.py @@ -18,7 +18,7 @@ def device_auth(): pass -def init(AUTH_METHOD, *args, **kwargs): +def init_auth(AUTH_METHOD, *args, **kwargs): auth_method = { PIN_AUTH: pin_auth, OAUTH_AUTH: oauth_auth, DEVICE_AUTH: device_auth } From 8b5ec76f3be9b9ae1ea41b45982aa94cf7c66e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:17:13 +0200 Subject: [PATCH 067/193] fixup! fixup! Add auth init() --- trakt/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trakt/auth.py b/trakt/auth.py index 9ccb09ec..e1895645 100644 --- a/trakt/auth.py +++ b/trakt/auth.py @@ -18,10 +18,10 @@ def device_auth(): pass -def init_auth(AUTH_METHOD, *args, **kwargs): - auth_method = { +def init_auth(method: str, *args, **kwargs): + methods = { PIN_AUTH: pin_auth, OAUTH_AUTH: oauth_auth, DEVICE_AUTH: device_auth } """Run the auth function specified by *AUTH_METHOD*""" - return auth_method.get(AUTH_METHOD, PIN_AUTH)(*args, **kwargs) + return methods.get(method, PIN_AUTH)(*args, **kwargs) From 502d3f4781d6631e13e41f1afef60406dff798bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:19:33 +0200 Subject: [PATCH 068/193] fixup! Add trakt/auth/device_auth.py module --- trakt/auth/device_auth.py | 189 +++++++++++++++++++++++++++----------- 1 file changed, 136 insertions(+), 53 deletions(-) diff --git a/trakt/auth/device_auth.py b/trakt/auth/device_auth.py index d1f727bd..b83240f1 100644 --- a/trakt/auth/device_auth.py +++ b/trakt/auth/device_auth.py @@ -1,58 +1,141 @@ -def device_auth(client_id=None, client_secret=None, store=False): - """Process for authenticating using device authentication. - - The function will attempt getting the device_id, and provide - the user with a url and code. After getting the device - id, a timer is started to poll periodic for a successful authentication. - This is a blocking action, meaning you - will not be able to run any other code, while waiting for an access token. - - If you want more control over the authentication flow, use the functions - get_device_code and get_device_token. - Where poll_for_device_token will check if the "offline" - authentication was successful. - - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious - :return: A dict with the authentication result. - Or False of authentication failed. - """ - error_messages = { - 404: 'Invalid device_code', - 409: 'You already approved this code', - 410: 'The tokens have expired, restart the process', - 418: 'You explicitly denied this code', - } - - success_message = ( - "You've been successfully authenticated. " - "With access_token {access_token} and refresh_token {refresh_token}" - ) - - response = get_device_code(client_id=client_id, - client_secret=client_secret) - device_code = response['device_code'] - interval = response['interval'] - - # No need to check for expiration, the API will notify us. - while True: - response = get_device_token(device_code, client_id, client_secret, - store) +import time - if response.status_code == 200: - print(success_message.format_map(response.json())) - break - elif response.status_code == 429: # slow down - interval *= 2 +class DeviceAuth: + def device_auth(client_id=None, client_secret=None, store=False): + """Process for authenticating using device authentication. + + The function will attempt getting the device_id, and provide + the user with a url and code. After getting the device + id, a timer is started to poll periodic for a successful authentication. + This is a blocking action, meaning you + will not be able to run any other code, while waiting for an access token. + + If you want more control over the authentication flow, use the functions + get_device_code and get_device_token. + Where poll_for_device_token will check if the "offline" + authentication was successful. + + :param client_id: Your Trakt OAuth Application's Client ID + :param client_secret: Your Trakt OAuth Application's Client Secret + :param store: Boolean flag used to determine if your trakt api auth data + should be stored locally on the system. Default is :const:`False` for + the security conscious + :return: A dict with the authentication result. + Or False of authentication failed. + """ + error_messages = { + 404: 'Invalid device_code', + 409: 'You already approved this code', + 410: 'The tokens have expired, restart the process', + 418: 'You explicitly denied this code', + } + + success_message = ( + "You've been successfully authenticated. " + "With access_token {access_token} and refresh_token {refresh_token}" + ) + + response = get_device_code(client_id=client_id, + client_secret=client_secret) + device_code = response['device_code'] + interval = response['interval'] + + # No need to check for expiration, the API will notify us. + while True: + response = get_device_token(device_code, client_id, client_secret, + store) + + if response.status_code == 200: + print(success_message.format_map(response.json())) + break + + elif response.status_code == 429: # slow down + interval *= 2 + + elif response.status_code != 400: # not pending + print(error_messages.get(response.status_code, response.reason)) + break + + time.sleep(interval) - elif response.status_code != 400: # not pending - print(error_messages.get(response.status_code, response.reason)) - break + return response + + def get_device_code(client_id=None, client_secret=None): + """Generate a device code, used for device oauth authentication. + + Trakt docs: https://trakt.docs.apiary.io/#reference/ + authentication-devices/device-code + :param client_id: Your Trakt OAuth Application's Client ID + :param client_secret: Your Trakt OAuth Application's Client Secret + :return: Your OAuth device code. + """ + global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN + if client_id is None and client_secret is None: + client_id, client_secret = _get_client_info() + CLIENT_ID, CLIENT_SECRET = client_id, client_secret + HEADERS['trakt-api-key'] = CLIENT_ID + + device_code_url = urljoin(BASE_URL, '/oauth/device/code') + headers = {'Content-Type': 'application/json'} + data = {"client_id": CLIENT_ID} + + device_response = session.post(device_code_url, + json=data, headers=headers).json() + print('Your user code is: {user_code}, please navigate to ' + '{verification_url} to authenticate'.format( + user_code=device_response.get('user_code'), + verification_url=device_response.get('verification_url') + )) + + device_response['requested'] = time.time() + return device_response + + def get_device_token(device_code, client_id=None, client_secret=None, + store=False): + """ + Trakt docs: https://trakt.docs.apiary.io/#reference/ + authentication-devices/get-token + Response: + { + "access_token": "", + "token_type": "bearer", + "expires_in": 7776000, + "refresh_token": "", + "scope": "public", + "created_at": 1519329051 + } + :return: Information regarding the authentication polling. + :return type: dict + """ + global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_REFRESH + if client_id is None and client_secret is None: + client_id, client_secret = _get_client_info() + CLIENT_ID, CLIENT_SECRET = client_id, client_secret + HEADERS['trakt-api-key'] = CLIENT_ID + + data = { + "code": device_code, + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET + } + + response = session.post( + urljoin(BASE_URL, '/oauth/device/token'), json=data + ) + + # We only get json on success. + if response.status_code == 200: + data = response.json() + OAUTH_TOKEN = data.get('access_token') + OAUTH_REFRESH = data.get('refresh_token') + OAUTH_EXPIRES_AT = data.get("created_at") + data.get("expires_in") - time.sleep(interval) + if store: + _store( + CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, + OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, + OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT + ) - return response + return response From 85c969a518383f0825f772271ae202dd0c2079ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:22:34 +0200 Subject: [PATCH 069/193] fixup! fixup! Add trakt/auth/device_auth.py module --- trakt/auth/device_auth.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/trakt/auth/device_auth.py b/trakt/auth/device_auth.py index b83240f1..81dc3481 100644 --- a/trakt/auth/device_auth.py +++ b/trakt/auth/device_auth.py @@ -2,7 +2,19 @@ class DeviceAuth: - def device_auth(client_id=None, client_secret=None, store=False): + def __init__(self, client_id=None, client_secret=None, store=False): + """ + :param client_id: Your Trakt OAuth Application's Client ID + :param client_secret: Your Trakt OAuth Application's Client Secret + :param store: Boolean flag used to determine if your trakt api auth data + should be stored locally on the system. Default is :const:`False` for + the security conscious + """ + self.client_id = client_id + self.client_secret = client_secret + self.store = store + + def authenticate(self): """Process for authenticating using device authentication. The function will attempt getting the device_id, and provide @@ -16,11 +28,6 @@ def device_auth(client_id=None, client_secret=None, store=False): Where poll_for_device_token will check if the "offline" authentication was successful. - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious :return: A dict with the authentication result. Or False of authentication failed. """ @@ -36,15 +43,13 @@ def device_auth(client_id=None, client_secret=None, store=False): "With access_token {access_token} and refresh_token {refresh_token}" ) - response = get_device_code(client_id=client_id, - client_secret=client_secret) + response = self.get_device_code(client_id=self.client_id, client_secret=self.client_secret) device_code = response['device_code'] interval = response['interval'] # No need to check for expiration, the API will notify us. while True: - response = get_device_token(device_code, client_id, client_secret, - store) + response = self.get_device_token(device_code, self.client_id, self.client_secret, self.store) if response.status_code == 200: print(success_message.format_map(response.json())) @@ -61,7 +66,7 @@ def device_auth(client_id=None, client_secret=None, store=False): return response - def get_device_code(client_id=None, client_secret=None): + def get_device_code(self, client_id=None, client_secret=None): """Generate a device code, used for device oauth authentication. Trakt docs: https://trakt.docs.apiary.io/#reference/ @@ -91,7 +96,7 @@ def get_device_code(client_id=None, client_secret=None): device_response['requested'] = time.time() return device_response - def get_device_token(device_code, client_id=None, client_secret=None, + def get_device_token(self, device_code, client_id=None, client_secret=None, store=False): """ Trakt docs: https://trakt.docs.apiary.io/#reference/ From 7b176ac434a0d915eaa6eeeb221d76849eef7ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:23:46 +0200 Subject: [PATCH 070/193] fixup! fixup! fixup! Add trakt/auth/device_auth.py module --- trakt/auth/{device_auth.py => device.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename trakt/auth/{device_auth.py => device.py} (100%) diff --git a/trakt/auth/device_auth.py b/trakt/auth/device.py similarity index 100% rename from trakt/auth/device_auth.py rename to trakt/auth/device.py From 84874376dc9f6b11d35d8271b0d5609f4d724245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:24:02 +0200 Subject: [PATCH 071/193] fixup! fixup! fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/{oauth_auth.py => oauth.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename trakt/auth/{oauth_auth.py => oauth.py} (100%) diff --git a/trakt/auth/oauth_auth.py b/trakt/auth/oauth.py similarity index 100% rename from trakt/auth/oauth_auth.py rename to trakt/auth/oauth.py From 4b09d70ab09efcdba389f5c694cd62cea1c4bff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:24:24 +0200 Subject: [PATCH 072/193] fixup! Add trakt/auth/pin_auth.py module --- trakt/auth/{pin_auth.py => pin.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename trakt/auth/{pin_auth.py => pin.py} (100%) diff --git a/trakt/auth/pin_auth.py b/trakt/auth/pin.py similarity index 100% rename from trakt/auth/pin_auth.py rename to trakt/auth/pin.py From af52f85113a827fb3a9d3af01750cc420594bea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:25:25 +0200 Subject: [PATCH 073/193] Setup auth module --- trakt/auth/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 trakt/auth/__init__.py diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/trakt/auth/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- From a5969e195ae9b699568b5074dfa0743200c84c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:26:58 +0200 Subject: [PATCH 074/193] fixup! Add auth module --- trakt/auth.py | 27 --------------------------- trakt/auth/__init__.py | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 27 deletions(-) delete mode 100644 trakt/auth.py diff --git a/trakt/auth.py b/trakt/auth.py deleted file mode 100644 index e1895645..00000000 --- a/trakt/auth.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -"""Authentication methods""" - -__author__ = 'Jon Nappi, Elan Ruusamäe' - -from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH - - -def pin_auth(): - pass - - -def oauth_auth(): - pass - - -def device_auth(): - pass - - -def init_auth(method: str, *args, **kwargs): - methods = { - PIN_AUTH: pin_auth, OAUTH_AUTH: oauth_auth, DEVICE_AUTH: device_auth - } - - """Run the auth function specified by *AUTH_METHOD*""" - return methods.get(method, PIN_AUTH)(*args, **kwargs) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 40a96afc..e1895645 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -1 +1,27 @@ # -*- coding: utf-8 -*- +"""Authentication methods""" + +__author__ = 'Jon Nappi, Elan Ruusamäe' + +from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH + + +def pin_auth(): + pass + + +def oauth_auth(): + pass + + +def device_auth(): + pass + + +def init_auth(method: str, *args, **kwargs): + methods = { + PIN_AUTH: pin_auth, OAUTH_AUTH: oauth_auth, DEVICE_AUTH: device_auth + } + + """Run the auth function specified by *AUTH_METHOD*""" + return methods.get(method, PIN_AUTH)(*args, **kwargs) From 562ad3520912d0c6261c299f19060b96cf921e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:28:25 +0200 Subject: [PATCH 075/193] Call DeviceAuth --- trakt/auth/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index e1895645..85aba96d 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -14,8 +14,10 @@ def oauth_auth(): pass -def device_auth(): - pass +def device_auth(*args, **kwargs): + from trakt.auth.device import DeviceAuth + + return DeviceAuth(*args, **kwargs).authenticate() def init_auth(method: str, *args, **kwargs): From 6dc9c9ec19e5c655e7518f010d3ac32727121f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:29:04 +0200 Subject: [PATCH 076/193] fixup! fixup! Add auth module --- trakt/auth/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 85aba96d..807827dc 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -22,7 +22,9 @@ def device_auth(*args, **kwargs): def init_auth(method: str, *args, **kwargs): methods = { - PIN_AUTH: pin_auth, OAUTH_AUTH: oauth_auth, DEVICE_AUTH: device_auth + PIN_AUTH: pin_auth, + OAUTH_AUTH: oauth_auth, + DEVICE_AUTH: device_auth, } """Run the auth function specified by *AUTH_METHOD*""" From c07cd4f32443f860017882038c39ba563c1f32a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:29:17 +0200 Subject: [PATCH 077/193] fixup! fixup! fixup! Add auth module --- trakt/auth/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 807827dc..97b8dae0 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -21,11 +21,12 @@ def device_auth(*args, **kwargs): def init_auth(method: str, *args, **kwargs): + """Run the auth function specified by *AUTH_METHOD*""" + methods = { PIN_AUTH: pin_auth, OAUTH_AUTH: oauth_auth, DEVICE_AUTH: device_auth, } - """Run the auth function specified by *AUTH_METHOD*""" return methods.get(method, PIN_AUTH)(*args, **kwargs) From d7b4423b08ac026b472141703b1140741b4787da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:32:17 +0200 Subject: [PATCH 078/193] fixup! fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth.py | 98 ++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 33be6684..9df6ce52 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -1,45 +1,53 @@ -def oauth_auth(username, client_id=None, client_secret=None, store=False, - oauth_cb=_terminal_oauth_pin): - """Generate an access_token to allow your application to authenticate via - OAuth - - :param username: Your trakt.tv username - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious - :param oauth_cb: Callback function to handle the retrieving of the OAuth - PIN. Default function `_terminal_oauth_pin` for terminal auth - :return: Your OAuth access token - """ - global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN - if client_id is None and client_secret is None: - client_id, client_secret = _get_client_info() - CLIENT_ID, CLIENT_SECRET = client_id, client_secret - HEADERS['trakt-api-key'] = CLIENT_ID - - authorization_base_url = urljoin(BASE_URL, '/oauth/authorize') - token_url = urljoin(BASE_URL, '/oauth/token') - - # OAuth endpoints given in the API documentation - oauth = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI, state=None) - - # Authorization URL to redirect user to Trakt for authorization - authorization_url, _ = oauth.authorization_url(authorization_base_url, - username=username) - - # Calling callback function to get the OAuth PIN - oauth_pin = oauth_cb(authorization_url) - - # Fetch, assign, and return the access token - oauth.fetch_token(token_url, client_secret=CLIENT_SECRET, code=oauth_pin) - OAUTH_TOKEN = oauth.token['access_token'] - OAUTH_REFRESH = oauth.token['refresh_token'] - OAUTH_EXPIRES_AT = oauth.token["created_at"] + oauth.token["expires_in"] - - if store: - _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, - OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT) - return OAUTH_TOKEN +class DeviceAuth: + def __init__(self, username, client_id=None, client_secret=None, store=False, oauth_cb=_terminal_oauth_pin): + """ + :param username: Your trakt.tv username + :param client_id: Your Trakt OAuth Application's Client ID + :param client_secret: Your Trakt OAuth Application's Client Secret + :param store: Boolean flag used to determine if your trakt api auth data + should be stored locally on the system. Default is :const:`False` for + the security conscious + :param oauth_cb: Callback function to handle the retrieving of the OAuth + PIN. Default function `_terminal_oauth_pin` for terminal auth + """ + self.username = username + self.client_id = client_id + self.client_secret = client_secret + self.store = store + self.oauth_cb = oauth_cb + + def authenticate(self): + """Generate an access_token to allow your application to authenticate via + OAuth + + :return: Your OAuth access token + """ + global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN + if self.client_id is None and self.client_secret is None: + self.client_id, self.client_secret = _get_client_info() + CLIENT_ID, CLIENT_SECRET = self.client_id, self.client_secret + HEADERS['trakt-api-key'] = CLIENT_ID + + authorization_base_url = urljoin(BASE_URL, '/oauth/authorize') + token_url = urljoin(BASE_URL, '/oauth/token') + + # OAuth endpoints given in the API documentation + oauth = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI, state=None) + + # Authorization URL to redirect user to Trakt for authorization + authorization_url, _ = oauth.authorization_url(authorization_base_url, username=self.username) + + # Calling callback function to get the OAuth PIN + oauth_pin = oauth_cb(authorization_url) + + # Fetch, assign, and return the access token + oauth.fetch_token(token_url, client_secret=CLIENT_SECRET, code=oauth_pin) + OAUTH_TOKEN = oauth.token['access_token'] + OAUTH_REFRESH = oauth.token['refresh_token'] + OAUTH_EXPIRES_AT = oauth.token["created_at"] + oauth.token["expires_in"] + + if self.store: + _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, + OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, + OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT) + return OAUTH_TOKEN From 8ef498785313dce385f6630b6bc1ff1fc606de4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:33:01 +0200 Subject: [PATCH 079/193] fixup! fixup! fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 9df6ce52..34000469 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -1,4 +1,4 @@ -class DeviceAuth: +class OAuth: def __init__(self, username, client_id=None, client_secret=None, store=False, oauth_cb=_terminal_oauth_pin): """ :param username: Your trakt.tv username From e4462932465b0ed63003718e2cdf5bb9dcfd7f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:33:24 +0200 Subject: [PATCH 080/193] fixup! fixup! Add auth module --- trakt/auth/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 97b8dae0..9e961bce 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -10,8 +10,10 @@ def pin_auth(): pass -def oauth_auth(): - pass +def oauth_auth(*args, **kwargs): + from trakt.auth.oauth import OAuth + + return OAuth(*args, **kwargs).authenticate() def device_auth(*args, **kwargs): From 03792d7e384cc619bd9e267523e16a972413db88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:35:39 +0200 Subject: [PATCH 081/193] fixup! Add trakt/auth/pin_auth.py module --- trakt/auth/pin.py | 88 ++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 1635615d..b20d646e 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -1,42 +1,52 @@ -def pin_auth(pin=None, client_id=None, client_secret=None, store=False): - """Generate an access_token from a Trakt API PIN code. +class PinAuth: + def __init__(self, pin=None, client_id=None, client_secret=None, store=False): + """ + :param pin: Optional Trakt API PIN code. If one is not specified, you will + be prompted to go generate one + :param client_id: The oauth client_id for authenticating to the trakt API. + :param client_secret: The oauth client_secret for authenticating to the + trakt API. + :param store: Boolean flag used to determine if your trakt api auth data + should be stored locally on the system. Default is :const:`False` for + the security conscious + """ + self.pin = pin + self.client_id = client_id + self.client_secret = client_secret + self.store = store - :param pin: Optional Trakt API PIN code. If one is not specified, you will - be prompted to go generate one - :param client_id: The oauth client_id for authenticating to the trakt API. - :param client_secret: The oauth client_secret for authenticating to the - trakt API. - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious - :return: Your OAuth access token - """ - global OAUTH_TOKEN, CLIENT_ID, CLIENT_SECRET - CLIENT_ID, CLIENT_SECRET = client_id, client_secret - if client_id is None and client_secret is None: - CLIENT_ID, CLIENT_SECRET = _get_client_info(app_id=True) - if pin is None and APPLICATION_ID is None: - print('You must set the APPLICATION_ID of the Trakt application you ' - 'wish to use. You can find this ID by visiting the following ' - 'URL.') - print('https://trakt.tv/oauth/applications') - sys.exit(1) - if pin is None: - print('If you do not have a Trakt.tv PIN, please visit the following ' - 'url and log in to generate one.') - pin_url = 'https://trakt.tv/pin/{id}'.format(id=APPLICATION_ID) - print(pin_url) - pin = input('Please enter your PIN: ') - args = {'code': pin, - 'redirect_uri': REDIRECT_URI, - 'grant_type': 'authorization_code', - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET} + def authenticate(self): + """Generate an access_token from a Trakt API PIN code. - response = session.post(''.join([BASE_URL, '/oauth/token']), data=args) - OAUTH_TOKEN = response.json().get('access_token', None) + :return: Your OAuth access token + """ - if store: - _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, APPLICATION_ID=APPLICATION_ID) - return OAUTH_TOKEN + global OAUTH_TOKEN, CLIENT_ID, CLIENT_SECRET + CLIENT_ID, CLIENT_SECRET = self.client_id, self.client_secret + if self.client_id is None and self.client_secret is None: + CLIENT_ID, CLIENT_SECRET = _get_client_info(app_id=True) + if self.pin is None and APPLICATION_ID is None: + print('You must set the APPLICATION_ID of the Trakt application you ' + 'wish to use. You can find this ID by visiting the following ' + 'URL.') + print('https://trakt.tv/oauth/applications') + sys.exit(1) + if self.pin is None: + print('If you do not have a Trakt.tv PIN, please visit the following ' + 'url and log in to generate one.') + pin_url = 'https://trakt.tv/pin/{id}'.format(id=APPLICATION_ID) + print(pin_url) + self.pin = input('Please enter your PIN: ') + args = {'code': self.pin, + 'redirect_uri': REDIRECT_URI, + 'grant_type': 'authorization_code', + 'client_id': CLIENT_ID, + 'client_secret': CLIENT_SECRET} + + response = session.post(''.join([BASE_URL, '/oauth/token']), data=args) + OAUTH_TOKEN = response.json().get('access_token', None) + + if store: + _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, + OAUTH_TOKEN=OAUTH_TOKEN, APPLICATION_ID=APPLICATION_ID) + return OAUTH_TOKEN From 91fa9285cdf366013505e6ffd2626e5bf4f5b83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:36:08 +0200 Subject: [PATCH 082/193] fixup! fixup! Add auth module --- trakt/auth/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 9e961bce..ddf87333 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -6,8 +6,10 @@ from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH -def pin_auth(): - pass +def pin_auth(*args, **kwargs): + from trakt.auth.pin import PinAuth + + return PinAuth(*args, **kwargs).authenticate() def oauth_auth(*args, **kwargs): From a0e618a948c1a5202578825ae790304a586a3678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:39:22 +0200 Subject: [PATCH 083/193] fixup! fixup! fixup! fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 34000469..2322beb3 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -1,5 +1,5 @@ class OAuth: - def __init__(self, username, client_id=None, client_secret=None, store=False, oauth_cb=_terminal_oauth_pin): + def __init__(self, username, client_id=None, client_secret=None, store=False, oauth_cb=None): """ :param username: Your trakt.tv username :param client_id: Your Trakt OAuth Application's Client ID @@ -14,7 +14,7 @@ def __init__(self, username, client_id=None, client_secret=None, store=False, oa self.client_id = client_id self.client_secret = client_secret self.store = store - self.oauth_cb = oauth_cb + self.oauth_cb = self.terminal_oauth_pin if oauth_cb is None else oauth_cb def authenticate(self): """Generate an access_token to allow your application to authenticate via @@ -38,7 +38,7 @@ def authenticate(self): authorization_url, _ = oauth.authorization_url(authorization_base_url, username=self.username) # Calling callback function to get the OAuth PIN - oauth_pin = oauth_cb(authorization_url) + oauth_pin = self.oauth_cb(authorization_url) # Fetch, assign, and return the access token oauth.fetch_token(token_url, client_secret=CLIENT_SECRET, code=oauth_pin) @@ -51,3 +51,17 @@ def authenticate(self): OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT) return OAUTH_TOKEN + + @staticmethod + def terminal_oauth_pin(authorization_url): + """Default OAuth callback used for terminal applications. + + :param authorization_url: Predefined url by function `oauth_auth`. URL will + be prompted to you in the terminal + :return: OAuth PIN + """ + print('Please go here and authorize,', authorization_url) + + # Get the authorization verifier code from the callback url + response = input('Paste the Code returned here: ') + return response From 8dccde5fc3f5839aaaeb0c058cce922224ec481f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:41:03 +0200 Subject: [PATCH 084/193] fixup! fixup! Add auth module --- trakt/auth/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index ddf87333..98f74b6b 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -24,6 +24,27 @@ def device_auth(*args, **kwargs): return DeviceAuth(*args, **kwargs).authenticate() +def get_client_info(app_id=False): + """Helper function to poll the user for Client ID and Client Secret + strings + + :return: A 2-tuple of client_id, client_secret + """ + global APPLICATION_ID + print('If you do not have a client ID and secret. Please visit the ' + 'following url to create them.') + print('http://trakt.tv/oauth/applications') + client_id = input('Please enter your client id: ') + client_secret = input('Please enter your client secret: ') + if app_id: + msg = 'Please enter your application ID ({default}): '.format( + default=APPLICATION_ID) + user_input = input(msg) + if user_input: + APPLICATION_ID = user_input + return client_id, client_secret + + def init_auth(method: str, *args, **kwargs): """Run the auth function specified by *AUTH_METHOD*""" From 44e634902f7f40e84f86929cb264ecfefc92317f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:41:20 +0200 Subject: [PATCH 085/193] fixup! fixup! Add trakt/auth/device_auth.py module --- trakt/auth/device.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 81dc3481..532393bf 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -1,5 +1,7 @@ import time +from trakt.auth import get_client_info + class DeviceAuth: def __init__(self, client_id=None, client_secret=None, store=False): @@ -77,7 +79,7 @@ def get_device_code(self, client_id=None, client_secret=None): """ global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN if client_id is None and client_secret is None: - client_id, client_secret = _get_client_info() + client_id, client_secret = get_client_info() CLIENT_ID, CLIENT_SECRET = client_id, client_secret HEADERS['trakt-api-key'] = CLIENT_ID @@ -115,7 +117,7 @@ def get_device_token(self, device_code, client_id=None, client_secret=None, """ global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_REFRESH if client_id is None and client_secret is None: - client_id, client_secret = _get_client_info() + client_id, client_secret = get_client_info() CLIENT_ID, CLIENT_SECRET = client_id, client_secret HEADERS['trakt-api-key'] = CLIENT_ID From c51c8c90f78ff1c56320e99bcf3b4d8306ee7096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:41:28 +0200 Subject: [PATCH 086/193] fixup! fixup! fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 2322beb3..3e88bd50 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -1,3 +1,6 @@ +from trakt.auth import get_client_info + + class OAuth: def __init__(self, username, client_id=None, client_secret=None, store=False, oauth_cb=None): """ @@ -24,7 +27,7 @@ def authenticate(self): """ global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN if self.client_id is None and self.client_secret is None: - self.client_id, self.client_secret = _get_client_info() + self.client_id, self.client_secret = get_client_info() CLIENT_ID, CLIENT_SECRET = self.client_id, self.client_secret HEADERS['trakt-api-key'] = CLIENT_ID From 05c2f123ded4736e74467ba0ce785d0733bbc435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:43:16 +0200 Subject: [PATCH 087/193] Add Config class --- trakt/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 trakt/config.py diff --git a/trakt/config.py b/trakt/config.py new file mode 100644 index 00000000..02dde9f5 --- /dev/null +++ b/trakt/config.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +"""Class for config loading/storing""" + +__author__ = 'Elan Ruusamäe' + + +class Config: + pass From 1ab597e15f0f0976d4725d913cad2f4e938ad0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:44:19 +0200 Subject: [PATCH 088/193] fixup! Add Config class --- trakt/config.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/trakt/config.py b/trakt/config.py index 02dde9f5..f15572b1 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -3,6 +3,17 @@ __author__ = 'Elan Ruusamäe' +import json + class Config: - pass + def __init__(self, config_path: str): + self.config_path = config_path + + def store(self, **kwargs): + """Helper function used to store Trakt configurations at ``CONFIG_PATH`` + + :param kwargs: Keyword args to store at ``CONFIG_PATH`` + """ + with open(self.config_path, 'w') as config_file: + json.dump(kwargs, config_file) From 8e113f0ccaf17a79417d97b290fc635ad8f4fb3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:45:30 +0200 Subject: [PATCH 089/193] Add config factory --- trakt/core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/trakt/core.py b/trakt/core.py index 35c72cb1..723b3c66 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -396,6 +396,13 @@ def get_config(): ) +@lru_cache(maxsize=None) +def config(): + from trakt.config import Config + + return Config(CONFIG_PATH) + + @lru_cache(maxsize=None) def api(): from trakt.api import HttpClient From 004d4764c651929ee0ab55be7c35b2556c55e4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:48:55 +0200 Subject: [PATCH 090/193] Add _store wrapper --- trakt/auth/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 98f74b6b..e805f02a 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -45,6 +45,12 @@ def get_client_info(app_id=False): return client_id, client_secret +def _store(**kwargs): + from trakt.core import config + + config().store(**kwargs) + + def init_auth(method: str, *args, **kwargs): """Run the auth function specified by *AUTH_METHOD*""" From 1766c72b8794fc4165df068a18ca23830d30bcd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:49:32 +0200 Subject: [PATCH 091/193] fixup! fixup! fixup! Add trakt/auth/device_auth.py module --- trakt/auth/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 532393bf..bab54345 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -1,6 +1,6 @@ import time -from trakt.auth import get_client_info +from trakt.auth import get_client_info, _store class DeviceAuth: From 9fda49b5f278ab73d52f34f9f46f33ae8450aee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:50:15 +0200 Subject: [PATCH 092/193] fixup! fixup! fixup! fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 3e88bd50..23c83e8f 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -1,4 +1,4 @@ -from trakt.auth import get_client_info +from trakt.auth import get_client_info, _store class OAuth: From a0bc9c83e32c88868044aec7b6662568827c6f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:50:32 +0200 Subject: [PATCH 093/193] fixup! fixup! Add trakt/auth/pin_auth.py module --- trakt/auth/pin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index b20d646e..217fb34c 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -1,3 +1,6 @@ +from trakt.auth import _store + + class PinAuth: def __init__(self, pin=None, client_id=None, client_secret=None, store=False): """ From e0bc0ee77a4289774a08186ab2955494cbedaa25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:51:07 +0200 Subject: [PATCH 094/193] fixup! fixup! Add trakt/auth/pin_auth.py module --- trakt/auth/pin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 217fb34c..852229e4 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -49,7 +49,7 @@ def authenticate(self): response = session.post(''.join([BASE_URL, '/oauth/token']), data=args) OAUTH_TOKEN = response.json().get('access_token', None) - if store: + if self.store: _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, OAUTH_TOKEN=OAUTH_TOKEN, APPLICATION_ID=APPLICATION_ID) return OAUTH_TOKEN From ca55cc423c36be0557365680998c1ba0aff1fb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:51:36 +0200 Subject: [PATCH 095/193] fixup! fixup! fixup! fixup! fixup! Add trakt/auth/oauth_auth.py module --- trakt/auth/oauth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 23c83e8f..beb1c388 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -1,4 +1,5 @@ from trakt.auth import get_client_info, _store +from requests_oauthlib import OAuth2Session class OAuth: From 5dc7ffbee0a5b4645bc193251dd20b51b93ef636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:54:22 +0200 Subject: [PATCH 096/193] fixup! Update test to test for something that is mocked as well --- tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 91c784e7..6c87f918 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,4 +13,4 @@ def test_api(): show = TVShow('Game of Thrones') assert show.title == 'Game of Thrones' - assert show.certification == 'TV-MA' \ No newline at end of file + assert show.certification == 'TV-MA' From c54661a1f9ec6b69edb4144bb345efb242b00157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 18:58:57 +0200 Subject: [PATCH 097/193] Cleanup unused code from Core --- trakt/core.py | 299 +------------------------------------------------- 1 file changed, 6 insertions(+), 293 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 723b3c66..52813a3a 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -2,27 +2,19 @@ """Objects, properties, and methods to be shared across other modules in the trakt package """ -import json -import logging import os from typing import NamedTuple, Optional -from urllib.parse import urljoin import requests -import sys -import time from collections import namedtuple -from functools import wraps, lru_cache -from requests_oauthlib import OAuth2Session -from datetime import datetime, timedelta, timezone -from trakt import errors +from functools import lru_cache __author__ = 'Jon Nappi' __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', 'REDIRECT_URI', 'HEADERS', 'CONFIG_PATH', 'OAUTH_TOKEN', - 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', 'api', - 'APPLICATION_ID', 'get_device_code', 'get_device_token'] + 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', 'api', 'config', + 'APPLICATION_ID'] #: The base url for the Trakt API. Can be modified to run against different #: Trakt.tv environments @@ -85,290 +77,11 @@ class AuthConfig(NamedTuple): REDIRECT_URI: str = REDIRECT_URI -def _store(**kwargs): - """Helper function used to store Trakt configurations at ``CONFIG_PATH`` - - :param kwargs: Keyword args to store at ``CONFIG_PATH`` - """ - with open(CONFIG_PATH, 'w') as config_file: - json.dump(kwargs, config_file) - - -def _get_client_info(app_id=False): - """Helper function to poll the user for Client ID and Client Secret - strings - - :return: A 2-tuple of client_id, client_secret - """ - global APPLICATION_ID - print('If you do not have a client ID and secret. Please visit the ' - 'following url to create them.') - print('http://trakt.tv/oauth/applications') - client_id = input('Please enter your client id: ') - client_secret = input('Please enter your client secret: ') - if app_id: - msg = 'Please enter your application ID ({default}): '.format( - default=APPLICATION_ID) - user_input = input(msg) - if user_input: - APPLICATION_ID = user_input - return client_id, client_secret - - -def pin_auth(pin=None, client_id=None, client_secret=None, store=False): - """Generate an access_token from a Trakt API PIN code. - - :param pin: Optional Trakt API PIN code. If one is not specified, you will - be prompted to go generate one - :param client_id: The oauth client_id for authenticating to the trakt API. - :param client_secret: The oauth client_secret for authenticating to the - trakt API. - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious - :return: Your OAuth access token - """ - global OAUTH_TOKEN, CLIENT_ID, CLIENT_SECRET - CLIENT_ID, CLIENT_SECRET = client_id, client_secret - if client_id is None and client_secret is None: - CLIENT_ID, CLIENT_SECRET = _get_client_info(app_id=True) - if pin is None and APPLICATION_ID is None: - print('You must set the APPLICATION_ID of the Trakt application you ' - 'wish to use. You can find this ID by visiting the following ' - 'URL.') - print('https://trakt.tv/oauth/applications') - sys.exit(1) - if pin is None: - print('If you do not have a Trakt.tv PIN, please visit the following ' - 'url and log in to generate one.') - pin_url = 'https://trakt.tv/pin/{id}'.format(id=APPLICATION_ID) - print(pin_url) - pin = input('Please enter your PIN: ') - args = {'code': pin, - 'redirect_uri': REDIRECT_URI, - 'grant_type': 'authorization_code', - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET} - - response = session.post(''.join([BASE_URL, '/oauth/token']), data=args) - OAUTH_TOKEN = response.json().get('access_token', None) - - if store: - _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, APPLICATION_ID=APPLICATION_ID) - return OAUTH_TOKEN - - -def _terminal_oauth_pin(authorization_url): - """Default OAuth callback used for terminal applications. - - :param authorization_url: Predefined url by function `oauth_auth`. URL will - be prompted to you in the terminal - :return: OAuth PIN - """ - print('Please go here and authorize,', authorization_url) - - # Get the authorization verifier code from the callback url - response = input('Paste the Code returned here: ') - return response - - -def oauth_auth(username, client_id=None, client_secret=None, store=False, - oauth_cb=_terminal_oauth_pin): - """Generate an access_token to allow your application to authenticate via - OAuth - - :param username: Your trakt.tv username - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious - :param oauth_cb: Callback function to handle the retrieving of the OAuth - PIN. Default function `_terminal_oauth_pin` for terminal auth - :return: Your OAuth access token - """ - global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN - if client_id is None and client_secret is None: - client_id, client_secret = _get_client_info() - CLIENT_ID, CLIENT_SECRET = client_id, client_secret - HEADERS['trakt-api-key'] = CLIENT_ID - - authorization_base_url = urljoin(BASE_URL, '/oauth/authorize') - token_url = urljoin(BASE_URL, '/oauth/token') - - # OAuth endpoints given in the API documentation - oauth = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI, state=None) - - # Authorization URL to redirect user to Trakt for authorization - authorization_url, _ = oauth.authorization_url(authorization_base_url, - username=username) - - # Calling callback function to get the OAuth PIN - oauth_pin = oauth_cb(authorization_url) - - # Fetch, assign, and return the access token - oauth.fetch_token(token_url, client_secret=CLIENT_SECRET, code=oauth_pin) - OAUTH_TOKEN = oauth.token['access_token'] - OAUTH_REFRESH = oauth.token['refresh_token'] - OAUTH_EXPIRES_AT = oauth.token["created_at"] + oauth.token["expires_in"] - - if store: - _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, - OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT) - return OAUTH_TOKEN - - -def get_device_code(client_id=None, client_secret=None): - """Generate a device code, used for device oauth authentication. - - Trakt docs: https://trakt.docs.apiary.io/#reference/ - authentication-devices/device-code - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret - :return: Your OAuth device code. - """ - global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN - if client_id is None and client_secret is None: - client_id, client_secret = _get_client_info() - CLIENT_ID, CLIENT_SECRET = client_id, client_secret - HEADERS['trakt-api-key'] = CLIENT_ID - - device_code_url = urljoin(BASE_URL, '/oauth/device/code') - headers = {'Content-Type': 'application/json'} - data = {"client_id": CLIENT_ID} - - device_response = session.post(device_code_url, - json=data, headers=headers).json() - print('Your user code is: {user_code}, please navigate to ' - '{verification_url} to authenticate'.format( - user_code=device_response.get('user_code'), - verification_url=device_response.get('verification_url') - )) - - device_response['requested'] = time.time() - return device_response - - -def get_device_token(device_code, client_id=None, client_secret=None, - store=False): - """ - Trakt docs: https://trakt.docs.apiary.io/#reference/ - authentication-devices/get-token - Response: - { - "access_token": "", - "token_type": "bearer", - "expires_in": 7776000, - "refresh_token": "", - "scope": "public", - "created_at": 1519329051 - } - :return: Information regarding the authentication polling. - :return type: dict - """ - global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_REFRESH - if client_id is None and client_secret is None: - client_id, client_secret = _get_client_info() - CLIENT_ID, CLIENT_SECRET = client_id, client_secret - HEADERS['trakt-api-key'] = CLIENT_ID - - data = { - "code": device_code, - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET - } - - response = session.post( - urljoin(BASE_URL, '/oauth/device/token'), json=data - ) - - # We only get json on success. - if response.status_code == 200: - data = response.json() - OAUTH_TOKEN = data.get('access_token') - OAUTH_REFRESH = data.get('refresh_token') - OAUTH_EXPIRES_AT = data.get("created_at") + data.get("expires_in") - - if store: - _store( - CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, - OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT - ) - - return response - - -def device_auth(client_id=None, client_secret=None, store=False): - """Process for authenticating using device authentication. - - The function will attempt getting the device_id, and provide - the user with a url and code. After getting the device - id, a timer is started to poll periodic for a successful authentication. - This is a blocking action, meaning you - will not be able to run any other code, while waiting for an access token. - - If you want more control over the authentication flow, use the functions - get_device_code and get_device_token. - Where poll_for_device_token will check if the "offline" - authentication was successful. - - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious - :return: A dict with the authentication result. - Or False of authentication failed. - """ - error_messages = { - 404: 'Invalid device_code', - 409: 'You already approved this code', - 410: 'The tokens have expired, restart the process', - 418: 'You explicitly denied this code', - } - - success_message = ( - "You've been successfully authenticated. " - "With access_token {access_token} and refresh_token {refresh_token}" - ) - - response = get_device_code(client_id=client_id, - client_secret=client_secret) - device_code = response['device_code'] - interval = response['interval'] - - # No need to check for expiration, the API will notify us. - while True: - response = get_device_token(device_code, client_id, client_secret, - store) - - if response.status_code == 200: - print(success_message.format_map(response.json())) - break - - elif response.status_code == 429: # slow down - interval *= 2 - - elif response.status_code != 400: # not pending - print(error_messages.get(response.status_code, response.reason)) - break - - time.sleep(interval) - - return response - - -auth_method = { - PIN_AUTH: pin_auth, OAUTH_AUTH: oauth_auth, DEVICE_AUTH: device_auth -} - - def init(*args, **kwargs): """Run the auth function specified by *AUTH_METHOD*""" - return auth_method.get(AUTH_METHOD, PIN_AUTH)(*args, **kwargs) + from trakt.auth import init_auth + + return init_auth(AUTH_METHOD, *args, **kwargs) Airs = namedtuple('Airs', ['day', 'time', 'timezone']) From e3963ac3cdd1df560d87b329fd23485a3e5723ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:01:28 +0200 Subject: [PATCH 098/193] Inline AuthConfig --- trakt/core.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 52813a3a..213f9e63 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -92,12 +92,23 @@ def init(*args, **kwargs): 'updated_at', 'likes', 'user_rating']) -def get_config(): +@lru_cache(maxsize=None) +def config(): + from trakt.config import Config + + return Config(CONFIG_PATH) + + +@lru_cache(maxsize=None) +def api(): + from trakt.api import HttpClient + from trakt.api import TokenAuth + global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT global OAUTH_REFRESH, OAUTH_TOKEN global CLIENT_ID, CLIENT_SECRET - return AuthConfig( + params = AuthConfig( CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, @@ -107,21 +118,6 @@ def get_config(): REDIRECT_URI=REDIRECT_URI, HEADERS=HEADERS, ) - - -@lru_cache(maxsize=None) -def config(): - from trakt.config import Config - - return Config(CONFIG_PATH) - - -@lru_cache(maxsize=None) -def api(): - from trakt.api import HttpClient - from trakt.api import TokenAuth - - params = get_config() client = HttpClient(BASE_URL, session) client.set_headers(params.HEADERS) auth = TokenAuth(client=client, config_path=CONFIG_PATH, params=params) From d0c62b045e705d59f9866531c2d3e1c7011705c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:38:53 +0200 Subject: [PATCH 099/193] Add exists method to Config --- trakt/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/trakt/config.py b/trakt/config.py index f15572b1..70ce31a5 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -4,12 +4,16 @@ __author__ = 'Elan Ruusamäe' import json +from os.path import exists class Config: def __init__(self, config_path: str): self.config_path = config_path + def exists(self): + return exists(self.config_path) + def store(self, **kwargs): """Helper function used to store Trakt configurations at ``CONFIG_PATH`` From 737509b4c1a721e05e056c775d320a5a40aece6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:40:41 +0200 Subject: [PATCH 100/193] Add load method to Config --- trakt/config.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/trakt/config.py b/trakt/config.py index 70ce31a5..d397556a 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -14,6 +14,16 @@ def __init__(self, config_path: str): def exists(self): return exists(self.config_path) + def load(self): + if not self.exists(): + return {} + + # Load in trakt API auth data from CONFIG_PATH + with open(self.config_path) as config_file: + config_data = json.load(config_file) + + return config_data + def store(self, **kwargs): """Helper function used to store Trakt configurations at ``CONFIG_PATH`` From b6c1469b21c3487634ded24ec7eb51efa5447888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:42:46 +0200 Subject: [PATCH 101/193] fixup! Add load method to Config --- trakt/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/trakt/config.py b/trakt/config.py index d397556a..4005af9e 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -15,6 +15,9 @@ def exists(self): return exists(self.config_path) def load(self): + """ + Load in trakt API auth data from CONFIG_PATH + """ if not self.exists(): return {} From 121043764350d76b34e19c4f15b5dd7106bb3e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:44:31 +0200 Subject: [PATCH 102/193] Use Config in TokenAuth --- trakt/api.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 32046f5a..8af9fbc0 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -14,6 +14,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' +from trakt.config import Config from trakt.core import AuthConfig from trakt.errors import OAuthException @@ -94,10 +95,10 @@ def error_map(self): class TokenAuth(dict, AuthBase): """Attaches Trakt.tv token Authentication to the given Request object.""" - def __init__(self, client: HttpClient, config_path: str, params: AuthConfig): + def __init__(self, client: HttpClient, config: Config, params: AuthConfig): super().__init__() + self.config = config self.client = client - self.config_path = config_path self.update(**params._asdict()) self.logger = logging.getLogger('trakt.api.oauth') @@ -166,29 +167,19 @@ def refresh_token(self): datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) ) ) - self.store_token( + self.config.store( CLIENT_ID=self['CLIENT_ID'], CLIENT_SECRET=self['CLIENT_SECRET'], OAUTH_TOKEN=self['OAUTH_TOKEN'], OAUTH_REFRESH=self['OAUTH_REFRESH'], OAUTH_EXPIRES_AT=self['OAUTH_EXPIRES_AT'], ) - def store_token(self, **kwargs): - """Helper function used to store Trakt configurations at ``CONFIG_PATH`` - - :param kwargs: Keyword args to store at ``CONFIG_PATH`` - """ - with open(self.config_path, 'w') as config_file: - json.dump(kwargs, config_file) - def load_config(self): """Manually load config from json config file.""" - if self['CLIENT_ID'] is not None and self['CLIENT_SECRET'] is not None or not os.path.exists(self.config_path): + if self['CLIENT_ID'] is not None and self['CLIENT_SECRET'] is not None or not self.config.exists(): return - # Load in trakt API auth data from CONFIG_PATH - with open(self.config_path) as config_file: - config_data = json.load(config_file) + config_data = self.config.load() keys = [ 'APPLICATION_ID', From 5b62b0b249cfbf061a28c7577f802cbf09ad8678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:44:35 +0200 Subject: [PATCH 103/193] Use Config in TokenAuth --- trakt/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/core.py b/trakt/core.py index 213f9e63..2967e01a 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -120,7 +120,7 @@ def api(): ) client = HttpClient(BASE_URL, session) client.set_headers(params.HEADERS) - auth = TokenAuth(client=client, config_path=CONFIG_PATH, params=params) + auth = TokenAuth(client=client, config=config(), params=params) client.set_auth(auth) return client From 7442f3f69f480a9cb0d3ab047463d0e9888ff0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:48:40 +0200 Subject: [PATCH 104/193] OAUTH_TOKEN_VALID Is not configurable property but state --- trakt/api.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 8af9fbc0..0057d120 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -95,6 +95,9 @@ def error_map(self): class TokenAuth(dict, AuthBase): """Attaches Trakt.tv token Authentication to the given Request object.""" + # OAuth token validity checked + OAUTH_TOKEN_VALID = None + def __init__(self, client: HttpClient, config: Config, params: AuthConfig): super().__init__() self.config = config @@ -117,8 +120,7 @@ def get_token(self): self.load_config() # Check token validity and refresh token if needed - if (not self['OAUTH_TOKEN_VALID'] and self['OAUTH_EXPIRES_AT'] is not None - and self['OAUTH_REFRESH'] is not None): + if not self.OAUTH_TOKEN_VALID and self['OAUTH_EXPIRES_AT'] is not None and self['OAUTH_REFRESH'] is not None: self.validate_token() return [ @@ -132,7 +134,7 @@ def validate_token(self): current = datetime.now(tz=timezone.utc) expires_at = datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) if expires_at - current > timedelta(days=2): - self['OAUTH_TOKEN_VALID'] = True + self.OAUTH_TOKEN_VALID = True else: self.refresh_token() @@ -160,7 +162,7 @@ def refresh_token(self): self['OAUTH_TOKEN'] = response.get("access_token") self['OAUTH_REFRESH'] = response.get("refresh_token") self['OAUTH_EXPIRES_AT'] = response.get("created_at") + response.get("expires_in") - self['OAUTH_TOKEN_VALID'] = True + self.OAUTH_TOKEN_VALID = True self.logger.info( "OAuth token successfully refreshed, valid until {}".format( From b095215929a9874d9c98ee159fd6b6ef805f6c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:48:47 +0200 Subject: [PATCH 105/193] OAUTH_TOKEN_VALID Is not configurable property but state --- trakt/core.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 2967e01a..c6510581 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -38,9 +38,6 @@ #: Your personal Trakt.tv OAUTH Bearer Token OAUTH_TOKEN = api_key = None -# OAuth token validity checked -OAUTH_TOKEN_VALID = None - # Your OAUTH token expiration date OAUTH_EXPIRES_AT = None @@ -72,7 +69,6 @@ class AuthConfig(NamedTuple): OAUTH_EXPIRES_AT: Optional[int] OAUTH_REFRESH: Optional[int] OAUTH_TOKEN: Optional[str] - OAUTH_TOKEN_VALID: Optional[bool] HEADERS: Optional[dict[str, str]] = HEADERS REDIRECT_URI: str = REDIRECT_URI @@ -104,7 +100,7 @@ def api(): from trakt.api import HttpClient from trakt.api import TokenAuth - global OAUTH_TOKEN_VALID, OAUTH_EXPIRES_AT + global OAUTH_EXPIRES_AT global OAUTH_REFRESH, OAUTH_TOKEN global CLIENT_ID, CLIENT_SECRET @@ -114,7 +110,6 @@ def api(): OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, OAUTH_REFRESH=OAUTH_REFRESH, OAUTH_TOKEN=OAUTH_TOKEN, - OAUTH_TOKEN_VALID=OAUTH_TOKEN_VALID, REDIRECT_URI=REDIRECT_URI, HEADERS=HEADERS, ) From f89ff00a190e3c7a60166be219b20d4b2bfcdc1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:57:10 +0200 Subject: [PATCH 106/193] Inline REDIRECT_URI (not configurable) --- trakt/core.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index c6510581..f2d6ad87 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -26,9 +26,6 @@ #: The Trakt.tv OAuth Client Secret for your OAuth Application CLIENT_SECRET = None -#: The OAuth2 Redirect URI for your OAuth Application -REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' - #: Default request HEADERS HEADERS = {'Content-Type': 'application/json', 'trakt-api-version': '2'} @@ -70,7 +67,8 @@ class AuthConfig(NamedTuple): OAUTH_REFRESH: Optional[int] OAUTH_TOKEN: Optional[str] HEADERS: Optional[dict[str, str]] = HEADERS - REDIRECT_URI: str = REDIRECT_URI + #: The OAuth2 Redirect URI for your OAuth Application + REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' def init(*args, **kwargs): From 61b923d0a62afb369fba4f7077ffa9beb370b86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:57:40 +0200 Subject: [PATCH 107/193] fixup! Inline REDIRECT_URI (not configurable) --- trakt/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/trakt/core.py b/trakt/core.py index f2d6ad87..9e65b559 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -108,7 +108,6 @@ def api(): OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, OAUTH_REFRESH=OAUTH_REFRESH, OAUTH_TOKEN=OAUTH_TOKEN, - REDIRECT_URI=REDIRECT_URI, HEADERS=HEADERS, ) client = HttpClient(BASE_URL, session) From 558b1531dbb0e4b1c4f7aa78d5d935f1893fcc52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:58:32 +0200 Subject: [PATCH 108/193] Disallow headers configurability --- trakt/api.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 0057d120..8c539fb7 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -23,12 +23,14 @@ class HttpClient: """Class for abstracting HTTP requests """ + #: Default request HEADERS + headers = {'Content-Type': 'application/json', 'trakt-api-version': '2'} + def __init__(self, base_url: str, session: Session): self.auth = None self.base_url = base_url self.session = session self.logger = logging.getLogger('trakt.http_client') - self.headers = {} def get(self, url: str): return self.request('get', url) @@ -42,9 +44,6 @@ def post(self, url: str, data): def put(self, url: str, data): return self.request('put', url, data=data) - def set_headers(self, headers): - self.headers.update(headers) - def set_auth(self, auth): self.auth = auth From c6fd8a2907ee1dda4e8188557b0bc27879649c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:59:01 +0200 Subject: [PATCH 109/193] fixup! fixup! Inline REDIRECT_URI (not configurable) --- trakt/core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 9e65b559..372f2ef3 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -12,7 +12,7 @@ __author__ = 'Jon Nappi' __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', - 'REDIRECT_URI', 'HEADERS', 'CONFIG_PATH', 'OAUTH_TOKEN', + 'HEADERS', 'CONFIG_PATH', 'OAUTH_TOKEN', 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', 'api', 'config', 'APPLICATION_ID'] @@ -66,7 +66,6 @@ class AuthConfig(NamedTuple): OAUTH_EXPIRES_AT: Optional[int] OAUTH_REFRESH: Optional[int] OAUTH_TOKEN: Optional[str] - HEADERS: Optional[dict[str, str]] = HEADERS #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' @@ -108,7 +107,6 @@ def api(): OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, OAUTH_REFRESH=OAUTH_REFRESH, OAUTH_TOKEN=OAUTH_TOKEN, - HEADERS=HEADERS, ) client = HttpClient(BASE_URL, session) client.set_headers(params.HEADERS) From 774ee78a9a479329b9a59251717f1d5e8dee5a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 19:59:14 +0200 Subject: [PATCH 110/193] Disallow headers configurability --- trakt/core.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 372f2ef3..2f0be5ca 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -12,7 +12,7 @@ __author__ = 'Jon Nappi' __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', - 'HEADERS', 'CONFIG_PATH', 'OAUTH_TOKEN', + 'CONFIG_PATH', 'OAUTH_TOKEN', 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', 'api', 'config', 'APPLICATION_ID'] @@ -26,9 +26,6 @@ #: The Trakt.tv OAuth Client Secret for your OAuth Application CLIENT_SECRET = None -#: Default request HEADERS -HEADERS = {'Content-Type': 'application/json', 'trakt-api-version': '2'} - #: Default path for where to store your trakt.tv API authentication information CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.pytrakt.json') From 6e2fa82d21ebfb872ca2f1ad3e77cf2d5e9804f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 20:00:08 +0200 Subject: [PATCH 111/193] fixup! Disallow headers configurability --- trakt/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/trakt/core.py b/trakt/core.py index 2f0be5ca..68938d89 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -106,7 +106,6 @@ def api(): OAUTH_TOKEN=OAUTH_TOKEN, ) client = HttpClient(BASE_URL, session) - client.set_headers(params.HEADERS) auth = TokenAuth(client=client, config=config(), params=params) client.set_auth(auth) From b843dc0885a54cb70685964af26e76f412853265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 20:00:30 +0200 Subject: [PATCH 112/193] Cosmetics --- trakt/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 8c539fb7..8588cf98 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -27,10 +27,10 @@ class HttpClient: headers = {'Content-Type': 'application/json', 'trakt-api-version': '2'} def __init__(self, base_url: str, session: Session): - self.auth = None self.base_url = base_url self.session = session self.logger = logging.getLogger('trakt.http_client') + self.auth = None def get(self, url: str): return self.request('get', url) From 6785998506b8ffc4984bd91ee6700ad9bf5c003e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 20:00:54 +0200 Subject: [PATCH 113/193] MaxSize=None --- trakt/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 8588cf98..2f008c41 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -79,7 +79,7 @@ def raise_if_needed(self, response): raise self.error_map[response.status_code](response) @property - @lru_cache(maxsize=1) + @lru_cache(maxsize=None) def error_map(self): """Map HTTP response codes to exception types """ From b86137950558af8036d075b997fc4cddebdf81d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:29:40 +0200 Subject: [PATCH 114/193] Update Config to extend AuthConfig --- trakt/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index 4005af9e..477a93dc 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -6,9 +6,11 @@ import json from os.path import exists +from trakt.core import AuthConfig -class Config: - def __init__(self, config_path: str): + +class Config(AuthConfig): + def __init__(self, config_path): self.config_path = config_path def exists(self): From b78cdc47f2e3ac2b77b64f1d235ab34b5ec49b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:30:29 +0200 Subject: [PATCH 115/193] Change AuthConfig to dataclass --- trakt/core.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 68938d89..45b76768 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -3,7 +3,8 @@ trakt package """ import os -from typing import NamedTuple, Optional +from dataclasses import dataclass +from typing import Optional import requests from collections import namedtuple @@ -57,7 +58,8 @@ session = requests.Session() -class AuthConfig(NamedTuple): +@dataclass +class AuthConfig: CLIENT_ID: Optional[str] CLIENT_SECRET: Optional[str] OAUTH_EXPIRES_AT: Optional[int] @@ -66,6 +68,12 @@ class AuthConfig(NamedTuple): #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' + def update(self, **kwargs): + for name, value in kwargs.items(): + self.__setattr__(name, value) + + return self + def init(*args, **kwargs): """Run the auth function specified by *AUTH_METHOD*""" From 5ed87705d3f8e4a1e459dfa8f612846e04f81cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:31:05 +0200 Subject: [PATCH 116/193] Update to use new Config --- trakt/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 45b76768..ce28f228 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -106,7 +106,7 @@ def api(): global OAUTH_REFRESH, OAUTH_TOKEN global CLIENT_ID, CLIENT_SECRET - params = AuthConfig( + params = dict( CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, @@ -114,7 +114,8 @@ def api(): OAUTH_TOKEN=OAUTH_TOKEN, ) client = HttpClient(BASE_URL, session) - auth = TokenAuth(client=client, config=config(), params=params) + c = config().update(**params) + auth = TokenAuth(client=client, config=c) client.set_auth(auth) return client From 14d69641db0a987ad30dd00b71bdce577dbf8ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:31:11 +0200 Subject: [PATCH 117/193] Update to use new Config --- trakt/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 2f008c41..01653649 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -97,11 +97,10 @@ class TokenAuth(dict, AuthBase): # OAuth token validity checked OAUTH_TOKEN_VALID = None - def __init__(self, client: HttpClient, config: Config, params: AuthConfig): + def __init__(self, client: HttpClient, config: Config): super().__init__() self.config = config self.client = client - self.update(**params._asdict()) self.logger = logging.getLogger('trakt.api.oauth') def __call__(self, r): From 3f3d7fc85997a36d1acfb8e4ae6103eacde4f4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:31:34 +0200 Subject: [PATCH 118/193] Export AuthConfig --- trakt/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/core.py b/trakt/core.py index ce28f228..29774070 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -13,7 +13,7 @@ __author__ = 'Jon Nappi' __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', - 'CONFIG_PATH', 'OAUTH_TOKEN', + 'CONFIG_PATH', 'OAUTH_TOKEN', 'AuthConfig', 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', 'api', 'config', 'APPLICATION_ID'] From 737fa1110527c178437715afcd3f160763ae0460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:45:10 +0200 Subject: [PATCH 119/193] Add get/set to AuthConfig --- trakt/core.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/trakt/core.py b/trakt/core.py index 29774070..85cd10d8 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -74,6 +74,12 @@ def update(self, **kwargs): return self + def get(self, name): + return self.__getattribute__(name) + + def set(self, name, value): + self.__setattr__(name, value) + def init(*args, **kwargs): """Run the auth function specified by *AUTH_METHOD*""" From ee2e8e440d71b34ffd7b6f9e9c2eea1bed31e16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:45:52 +0200 Subject: [PATCH 120/193] Update TokenAuth to use properties of Config --- trakt/api.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 01653649..d0ee4f34 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -118,7 +118,7 @@ def get_token(self): self.load_config() # Check token validity and refresh token if needed - if not self.OAUTH_TOKEN_VALID and self['OAUTH_EXPIRES_AT'] is not None and self['OAUTH_REFRESH'] is not None: + if not self.OAUTH_TOKEN_VALID and self.config.OAUTH_EXPIRES_AT is not None and self.config.OAUTH_REFRESH is not None: self.validate_token() return [ @@ -130,7 +130,7 @@ def validate_token(self): """Check if current OAuth token has not expired""" current = datetime.now(tz=timezone.utc) - expires_at = datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) + expires_at = datetime.fromtimestamp(self.config.OAUTH_EXPIRES_AT, tz=timezone.utc) if expires_at - current > timedelta(days=2): self.OAUTH_TOKEN_VALID = True else: @@ -141,10 +141,10 @@ def refresh_token(self): self.logger.info("OAuth token has expired, refreshing now...") data = { - 'client_id': self['CLIENT_ID'], - 'client_secret': self['CLIENT_SECRET'], - 'refresh_token': self['OAUTH_REFRESH'], - 'redirect_uri': self['REDIRECT_URI'], + 'client_id': self.config.CLIENT_ID, + 'client_secret': self.config.CLIENT_SECRET, + 'refresh_token': self.config.OAUTH_REFRESH, + 'redirect_uri': self.config.REDIRECT_URI, 'grant_type': 'refresh_token' } @@ -157,9 +157,9 @@ def refresh_token(self): ) return - self['OAUTH_TOKEN'] = response.get("access_token") - self['OAUTH_REFRESH'] = response.get("refresh_token") - self['OAUTH_EXPIRES_AT'] = response.get("created_at") + response.get("expires_in") + self.config.OAUTH_TOKEN = response.get("access_token") + self.config.OAUTH_REFRESH = response.get("refresh_token") + self.config.OAUTH_EXPIRES_AT = response.get("created_at") + response.get("expires_in") self.OAUTH_TOKEN_VALID = True self.logger.info( @@ -168,15 +168,17 @@ def refresh_token(self): ) ) self.config.store( - CLIENT_ID=self['CLIENT_ID'], CLIENT_SECRET=self['CLIENT_SECRET'], - OAUTH_TOKEN=self['OAUTH_TOKEN'], OAUTH_REFRESH=self['OAUTH_REFRESH'], - OAUTH_EXPIRES_AT=self['OAUTH_EXPIRES_AT'], + CLIENT_ID=self.config.CLIENT_ID, + CLIENT_SECRET=self.config.CLIENT_SECRET, + OAUTH_TOKEN=self.config.OAUTH_TOKEN, + OAUTH_REFRESH=self.config.OAUTH_REFRESH, + OAUTH_EXPIRES_AT=self.config.OAUTH_EXPIRES_AT, ) def load_config(self): """Manually load config from json config file.""" - if self['CLIENT_ID'] is not None and self['CLIENT_SECRET'] is not None or not self.config.exists(): + if self.config.CLIENT_ID is not None and self.config.CLIENT_SECRET is not None or not self.config.exists(): return config_data = self.config.load() @@ -191,7 +193,7 @@ def load_config(self): ] for key in keys: - if self[key] is not None: + if self.config.get(key) is not None: continue - self[key] = config_data.get(key, None) + self.config.set(key, config_data.get(key, None)) From f28c14717e9fbbe078b41541530cacc5a96d97cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:50:12 +0200 Subject: [PATCH 121/193] Remove dict parent, confuses callable check --- trakt/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index d0ee4f34..2794d3d6 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -91,7 +91,7 @@ def error_map(self): return {err.http_code: err for err in errs} -class TokenAuth(dict, AuthBase): +class TokenAuth(AuthBase): """Attaches Trakt.tv token Authentication to the given Request object.""" # OAuth token validity checked From 8eed801cfefae90e564e84caf5599cd775faaebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:52:14 +0200 Subject: [PATCH 122/193] More dict fixes --- trakt/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 2794d3d6..4415d93f 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -122,8 +122,8 @@ def get_token(self): self.validate_token() return [ - self['CLIENT_ID'], - self['OAUTH_TOKEN'], + self.config.CLIENT_ID, + self.config.OAUTH_TOKEN, ] def validate_token(self): @@ -164,7 +164,7 @@ def refresh_token(self): self.logger.info( "OAuth token successfully refreshed, valid until {}".format( - datetime.fromtimestamp(self['OAUTH_EXPIRES_AT'], tz=timezone.utc) + datetime.fromtimestamp(self.config.OAUTH_EXPIRES_AT, tz=timezone.utc) ) ) self.config.store( From 4109adb3ce60f1192a837c28d46732e33c6c64f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:58:09 +0200 Subject: [PATCH 123/193] Merge AuthConfig and Config --- trakt/config.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index 477a93dc..8bd3bd9a 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -4,15 +4,36 @@ __author__ = 'Elan Ruusamäe' import json +from dataclasses import dataclass from os.path import exists +from typing import Optional -from trakt.core import AuthConfig +@dataclass +class AuthConfig: + CLIENT_ID: Optional[str] + CLIENT_SECRET: Optional[str] + OAUTH_EXPIRES_AT: Optional[int] + OAUTH_REFRESH: Optional[int] + OAUTH_TOKEN: Optional[str] + #: The OAuth2 Redirect URI for your OAuth Application + REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' -class Config(AuthConfig): def __init__(self, config_path): self.config_path = config_path + def update(self, **kwargs): + for name, value in kwargs.items(): + self.__setattr__(name, value) + + return self + + def get(self, name): + return self.__getattribute__(name) + + def set(self, name, value): + self.__setattr__(name, value) + def exists(self): return exists(self.config_path) From 039dece2fe4eb660d33df507a65f9ae6b8fb3b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:58:20 +0200 Subject: [PATCH 124/193] Merge AuthConfig and Config --- trakt/core.py | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 85cd10d8..e3e7448b 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -13,7 +13,7 @@ __author__ = 'Jon Nappi' __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', - 'CONFIG_PATH', 'OAUTH_TOKEN', 'AuthConfig', + 'CONFIG_PATH', 'OAUTH_TOKEN', 'OAUTH_REFRESH', 'PIN_AUTH', 'OAUTH_AUTH', 'AUTH_METHOD', 'api', 'config', 'APPLICATION_ID'] @@ -58,29 +58,6 @@ session = requests.Session() -@dataclass -class AuthConfig: - CLIENT_ID: Optional[str] - CLIENT_SECRET: Optional[str] - OAUTH_EXPIRES_AT: Optional[int] - OAUTH_REFRESH: Optional[int] - OAUTH_TOKEN: Optional[str] - #: The OAuth2 Redirect URI for your OAuth Application - REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' - - def update(self, **kwargs): - for name, value in kwargs.items(): - self.__setattr__(name, value) - - return self - - def get(self, name): - return self.__getattribute__(name) - - def set(self, name, value): - self.__setattr__(name, value) - - def init(*args, **kwargs): """Run the auth function specified by *AUTH_METHOD*""" from trakt.auth import init_auth @@ -98,9 +75,9 @@ def init(*args, **kwargs): @lru_cache(maxsize=None) def config(): - from trakt.config import Config + from trakt.config import AuthConfig - return Config(CONFIG_PATH) + return AuthConfig(CONFIG_PATH) @lru_cache(maxsize=None) @@ -120,8 +97,7 @@ def api(): OAUTH_TOKEN=OAUTH_TOKEN, ) client = HttpClient(BASE_URL, session) - c = config().update(**params) - auth = TokenAuth(client=client, config=c) + auth = TokenAuth(client=client, config=config().update(**params)) client.set_auth(auth) return client From 67530e955e153400253a1eae2034ab2fc3fbbb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:58:26 +0200 Subject: [PATCH 125/193] Merge AuthConfig and Config --- trakt/api.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 4415d93f..920c492d 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -14,8 +14,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' -from trakt.config import Config -from trakt.core import AuthConfig +from trakt.config import AuthConfig from trakt.errors import OAuthException @@ -97,7 +96,7 @@ class TokenAuth(AuthBase): # OAuth token validity checked OAUTH_TOKEN_VALID = None - def __init__(self, client: HttpClient, config: Config): + def __init__(self, client: HttpClient, config: AuthConfig): super().__init__() self.config = config self.client = client From 6e27231a65a669504a7fd8f0b1762438ad87d96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 22:59:39 +0200 Subject: [PATCH 126/193] Add default param for config.get --- trakt/config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index 8bd3bd9a..de79f755 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -28,8 +28,11 @@ def update(self, **kwargs): return self - def get(self, name): - return self.__getattribute__(name) + def get(self, name, default=None): + try: + return self.__getattribute__(name) + except AttributeError: + return default def set(self, name, value): self.__setattr__(name, value) From dd3b4edeae9a234e1436c2168401694724285d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:01:58 +0200 Subject: [PATCH 127/193] Add have_refresh_token helper --- trakt/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/trakt/config.py b/trakt/config.py index de79f755..d59be67a 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -22,6 +22,10 @@ class AuthConfig: def __init__(self, config_path): self.config_path = config_path + def have_refresh_token(self): + # Check token validity and refresh token if needed + return self.OAUTH_EXPIRES_AT is not None and self.OAUTH_REFRESH is not None + def update(self, **kwargs): for name, value in kwargs.items(): self.__setattr__(name, value) From 3dde453a7d1a65613b98426acfe583e5b73e20c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:04:03 +0200 Subject: [PATCH 128/193] Use have_refresh_token --- trakt/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 920c492d..27c49431 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -117,7 +117,7 @@ def get_token(self): self.load_config() # Check token validity and refresh token if needed - if not self.OAUTH_TOKEN_VALID and self.config.OAUTH_EXPIRES_AT is not None and self.config.OAUTH_REFRESH is not None: + if not self.OAUTH_TOKEN_VALID and self.config.have_refresh_token(): self.validate_token() return [ From 9adb5864f11b904b35145c193c59f3ac9fdb8931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:04:13 +0200 Subject: [PATCH 129/193] Bulk update config --- trakt/api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 27c49431..c2c60feb 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -156,9 +156,11 @@ def refresh_token(self): ) return - self.config.OAUTH_TOKEN = response.get("access_token") - self.config.OAUTH_REFRESH = response.get("refresh_token") - self.config.OAUTH_EXPIRES_AT = response.get("created_at") + response.get("expires_in") + self.config.update( + OAUTH_TOKEN=response.get("access_token"), + OAUTH_REFRESH=response.get("refresh_token"), + OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"), + ) self.OAUTH_TOKEN_VALID = True self.logger.info( From 8603a8aef93587069230bb9514b1f52cc450aa76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:06:05 +0200 Subject: [PATCH 130/193] Store internal properties --- trakt/api.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index c2c60feb..cc3f53c3 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -168,13 +168,7 @@ def refresh_token(self): datetime.fromtimestamp(self.config.OAUTH_EXPIRES_AT, tz=timezone.utc) ) ) - self.config.store( - CLIENT_ID=self.config.CLIENT_ID, - CLIENT_SECRET=self.config.CLIENT_SECRET, - OAUTH_TOKEN=self.config.OAUTH_TOKEN, - OAUTH_REFRESH=self.config.OAUTH_REFRESH, - OAUTH_EXPIRES_AT=self.config.OAUTH_EXPIRES_AT, - ) + self.config.store() def load_config(self): """Manually load config from json config file.""" From f1c3ecc7f634495802eecea0c161a940a11f8c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:06:10 +0200 Subject: [PATCH 131/193] Store internal properties --- trakt/config.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index d59be67a..6a4f28af 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -57,10 +57,16 @@ def load(self): return config_data - def store(self, **kwargs): - """Helper function used to store Trakt configurations at ``CONFIG_PATH`` - - :param kwargs: Keyword args to store at ``CONFIG_PATH`` + def store(self): + """Store Trakt configurations at ``CONFIG_PATH`` """ + config = dict( + CLIENT_ID=self.CLIENT_ID, + CLIENT_SECRET=self.CLIENT_SECRET, + OAUTH_TOKEN=self.OAUTH_TOKEN, + OAUTH_REFRESH=self.OAUTH_REFRESH, + OAUTH_EXPIRES_AT=self.OAUTH_EXPIRES_AT, + ) + with open(self.config_path, 'w') as config_file: - json.dump(kwargs, config_file) + json.dump(config, config_file) From 2d3cf501ccfc39e2f946de9f55a564c03f7e0167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:10:09 +0200 Subject: [PATCH 132/193] Load config by Config class --- trakt/config.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index 6a4f28af..72bd0f0f 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -48,14 +48,27 @@ def load(self): """ Load in trakt API auth data from CONFIG_PATH """ - if not self.exists(): - return {} + if self.CLIENT_ID and self.CLIENT_SECRET or not self.exists(): + return # Load in trakt API auth data from CONFIG_PATH with open(self.config_path) as config_file: config_data = json.load(config_file) - return config_data + keys = [ + 'APPLICATION_ID', + 'CLIENT_ID', + 'CLIENT_SECRET', + 'OAUTH_EXPIRES_AT', + 'OAUTH_REFRESH', + 'OAUTH_TOKEN', + ] + + for key in keys: + if self.get(key) is not None: + continue + + self.set(key, config_data.get(key, None)) def store(self): """Store Trakt configurations at ``CONFIG_PATH`` From 5ee895c0e82de4e841775c93d8da3c60a4981452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:11:28 +0200 Subject: [PATCH 133/193] Sort list --- trakt/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index 72bd0f0f..f0d41333 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -56,12 +56,12 @@ def load(self): config_data = json.load(config_file) keys = [ - 'APPLICATION_ID', + # 'APPLICATION_ID', # FIXME: never saved? 'CLIENT_ID', 'CLIENT_SECRET', - 'OAUTH_EXPIRES_AT', - 'OAUTH_REFRESH', 'OAUTH_TOKEN', + 'OAUTH_REFRESH', + 'OAUTH_EXPIRES_AT', ] for key in keys: From 49958cac50d63333eca3188cebbf975aedebe72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:13:07 +0200 Subject: [PATCH 134/193] Use config class load --- trakt/api.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index cc3f53c3..abca41e4 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -115,7 +115,7 @@ def get_token(self): """Return client_id, client_token pair needed for Trakt.tv authentication """ - self.load_config() + self.config.load() # Check token validity and refresh token if needed if not self.OAUTH_TOKEN_VALID and self.config.have_refresh_token(): self.validate_token() @@ -170,25 +170,3 @@ def refresh_token(self): ) self.config.store() - def load_config(self): - """Manually load config from json config file.""" - - if self.config.CLIENT_ID is not None and self.config.CLIENT_SECRET is not None or not self.config.exists(): - return - - config_data = self.config.load() - - keys = [ - 'APPLICATION_ID', - 'CLIENT_ID', - 'CLIENT_SECRET', - 'OAUTH_EXPIRES_AT', - 'OAUTH_REFRESH', - 'OAUTH_TOKEN', - ] - - for key in keys: - if self.config.get(key) is not None: - continue - - self.config.set(key, config_data.get(key, None)) From 0700f900bf0d1b084378c66af030b7b626ed1eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:16:06 +0200 Subject: [PATCH 135/193] Use magic __annotations__ --- trakt/config.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index f0d41333..4386da71 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -55,16 +55,7 @@ def load(self): with open(self.config_path) as config_file: config_data = json.load(config_file) - keys = [ - # 'APPLICATION_ID', # FIXME: never saved? - 'CLIENT_ID', - 'CLIENT_SECRET', - 'OAUTH_TOKEN', - 'OAUTH_REFRESH', - 'OAUTH_EXPIRES_AT', - ] - - for key in keys: + for key in self.__annotations__.keys(): if self.get(key) is not None: continue From e123edddf9d0a016269ade0894b92080fe621f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:18:50 +0200 Subject: [PATCH 136/193] Use magic to save config --- trakt/config.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index 4386da71..6037656d 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -61,16 +61,18 @@ def load(self): self.set(key, config_data.get(key, None)) + def all(self): + result = {} + for key in self.__annotations__.keys(): + result[key] = self.get(key) + + del result['REDIRECT_URI'] # FIXME. remove + return result + def store(self): """Store Trakt configurations at ``CONFIG_PATH`` """ - config = dict( - CLIENT_ID=self.CLIENT_ID, - CLIENT_SECRET=self.CLIENT_SECRET, - OAUTH_TOKEN=self.OAUTH_TOKEN, - OAUTH_REFRESH=self.OAUTH_REFRESH, - OAUTH_EXPIRES_AT=self.OAUTH_EXPIRES_AT, - ) + config = self.all() with open(self.config_path, 'w') as config_file: json.dump(config, config_file) From 81a07949bf8e31f4c151756f8de7d79858f41c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:20:13 +0200 Subject: [PATCH 137/193] Remove REDIRECT_URI from AuthConfig --- trakt/api.py | 5 ++++- trakt/config.py | 3 --- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index abca41e4..32ea2c71 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -96,6 +96,9 @@ class TokenAuth(AuthBase): # OAuth token validity checked OAUTH_TOKEN_VALID = None + #: The OAuth2 Redirect URI for your OAuth Application + REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' + def __init__(self, client: HttpClient, config: AuthConfig): super().__init__() self.config = config @@ -143,7 +146,7 @@ def refresh_token(self): 'client_id': self.config.CLIENT_ID, 'client_secret': self.config.CLIENT_SECRET, 'refresh_token': self.config.OAUTH_REFRESH, - 'redirect_uri': self.config.REDIRECT_URI, + 'redirect_uri': self.REDIRECT_URI, 'grant_type': 'refresh_token' } diff --git a/trakt/config.py b/trakt/config.py index 6037656d..3de9ff4b 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -16,8 +16,6 @@ class AuthConfig: OAUTH_EXPIRES_AT: Optional[int] OAUTH_REFRESH: Optional[int] OAUTH_TOKEN: Optional[str] - #: The OAuth2 Redirect URI for your OAuth Application - REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' def __init__(self, config_path): self.config_path = config_path @@ -66,7 +64,6 @@ def all(self): for key in self.__annotations__.keys(): result[key] = self.get(key) - del result['REDIRECT_URI'] # FIXME. remove return result def store(self): From 354173b7e0040e7126e9514e243fd45ad285943e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:22:06 +0200 Subject: [PATCH 138/193] Cosmetics --- trakt/config.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index 3de9ff4b..4e0be589 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -24,12 +24,6 @@ def have_refresh_token(self): # Check token validity and refresh token if needed return self.OAUTH_EXPIRES_AT is not None and self.OAUTH_REFRESH is not None - def update(self, **kwargs): - for name, value in kwargs.items(): - self.__setattr__(name, value) - - return self - def get(self, name, default=None): try: return self.__getattribute__(name) @@ -39,32 +33,36 @@ def get(self, name, default=None): def set(self, name, value): self.__setattr__(name, value) - def exists(self): - return exists(self.config_path) + def update(self, **kwargs): + for name, value in kwargs.items(): + self.__setattr__(name, value) + + return self + + def all(self): + result = {} + for key in self.__annotations__.keys(): + result[key] = self.get(key) + + return result def load(self): """ Load in trakt API auth data from CONFIG_PATH """ - if self.CLIENT_ID and self.CLIENT_SECRET or not self.exists(): + if self.CLIENT_ID and self.CLIENT_SECRET or not exists(self.config_path): return - # Load in trakt API auth data from CONFIG_PATH with open(self.config_path) as config_file: config_data = json.load(config_file) for key in self.__annotations__.keys(): + # Don't overwrite if self.get(key) is not None: continue - self.set(key, config_data.get(key, None)) - - def all(self): - result = {} - for key in self.__annotations__.keys(): - result[key] = self.get(key) - - return result + value = config_data.get(key, None) + self.set(key, value) def store(self): """Store Trakt configurations at ``CONFIG_PATH`` From 84897e78d7ce97831d9aa5ccc56f42d08210d12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:22:33 +0200 Subject: [PATCH 139/193] Simplify --- trakt/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trakt/config.py b/trakt/config.py index 4e0be589..a034cb55 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -21,8 +21,7 @@ def __init__(self, config_path): self.config_path = config_path def have_refresh_token(self): - # Check token validity and refresh token if needed - return self.OAUTH_EXPIRES_AT is not None and self.OAUTH_REFRESH is not None + return self.OAUTH_EXPIRES_AT and self.OAUTH_REFRESH def get(self, name, default=None): try: From 5d78505c09556a11165627c5edd0d08964dc03cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:23:19 +0200 Subject: [PATCH 140/193] Docstring update --- trakt/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/config.py b/trakt/config.py index a034cb55..6415297e 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Class for config loading/storing""" +"""Class for trakt.tv auth config""" __author__ = 'Elan Ruusamäe' From 1919e8b0469c9ce763e07a5143e0158c2e960e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:25:24 +0200 Subject: [PATCH 141/193] Cleanup imports --- trakt/api.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 32ea2c71..668df836 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -2,15 +2,13 @@ """Interfaces to all of the People objects offered by the Trakt.tv API""" import json import logging -import os -from datetime import datetime, timezone, timedelta -from functools import lru_cache, wraps -from typing import NamedTuple, List, Optional -from urllib.parse import urljoin +from datetime import datetime, timedelta, timezone +from functools import lru_cache + +from requests import Session from requests.auth import AuthBase from trakt import errors -from requests import Session __author__ = 'Jon Nappi, Elan Ruusamäe' From 6e7d12622a06ee573ca310438b2ed2719300430f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:26:35 +0200 Subject: [PATCH 142/193] Global not needed? --- trakt/core.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index e3e7448b..712b88a4 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -85,10 +85,6 @@ def api(): from trakt.api import HttpClient from trakt.api import TokenAuth - global OAUTH_EXPIRES_AT - global OAUTH_REFRESH, OAUTH_TOKEN - global CLIENT_ID, CLIENT_SECRET - params = dict( CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, From 75c89bc92509c5e2749c0da802977aacc6c33f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:27:05 +0200 Subject: [PATCH 143/193] Cleanup imports --- trakt/core.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 712b88a4..43f6ebe4 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -3,13 +3,11 @@ trakt package """ import os -from dataclasses import dataclass -from typing import Optional - -import requests from collections import namedtuple from functools import lru_cache +import requests + __author__ = 'Jon Nappi' __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', @@ -82,8 +80,7 @@ def config(): @lru_cache(maxsize=None) def api(): - from trakt.api import HttpClient - from trakt.api import TokenAuth + from trakt.api import HttpClient, TokenAuth params = dict( CLIENT_ID=CLIENT_ID, From cff1531973161226af50dddb588bbe5d551bb634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:29:16 +0200 Subject: [PATCH 144/193] Config update from config method --- trakt/core.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 43f6ebe4..339a9972 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -75,22 +75,21 @@ def init(*args, **kwargs): def config(): from trakt.config import AuthConfig - return AuthConfig(CONFIG_PATH) - - -@lru_cache(maxsize=None) -def api(): - from trakt.api import HttpClient, TokenAuth - - params = dict( + return AuthConfig(CONFIG_PATH).update( CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, OAUTH_REFRESH=OAUTH_REFRESH, OAUTH_TOKEN=OAUTH_TOKEN, ) + + +@lru_cache(maxsize=None) +def api(): + from trakt.api import HttpClient, TokenAuth + client = HttpClient(BASE_URL, session) - auth = TokenAuth(client=client, config=config().update(**params)) + auth = TokenAuth(client=client, config=config()) client.set_auth(auth) return client From ae8a95364cd49e91e90114f498d385931af84092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:36:52 +0200 Subject: [PATCH 145/193] Fix logger name --- trakt/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 668df836..20f4b6b2 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -101,7 +101,7 @@ def __init__(self, client: HttpClient, config: AuthConfig): super().__init__() self.config = config self.client = client - self.logger = logging.getLogger('trakt.api.oauth') + self.logger = logging.getLogger('trakt.api.token_auth') def __call__(self, r): [client_id, client_token] = self.get_token() From 943b379cc8904515b82433473d6e2d8cdcb9dd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:43:28 +0200 Subject: [PATCH 146/193] Drop unused Errors class --- trakt/errors.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/trakt/errors.py b/trakt/errors.py index c9408e46..8a54cd48 100644 --- a/trakt/errors.py +++ b/trakt/errors.py @@ -20,26 +20,6 @@ 'TraktUnavailable', ] -from functools import lru_cache - - -class Errors: - def raise_if_needed(self, response): - if response.status_code in self.error_map: - raise self.error_map[response.status_code](response) - - @lru_cache(maxsize=None) - def error_map(self): - """Map HTTP response codes to exception types - """ - import sys - module = sys.modules[__name__] - # Get all of our exceptions except the base exception - errs = [getattr(module, att) for att in __all__ - if att != 'TraktException'] - - return {err.http_code: err for err in errs} - class TraktException(Exception): """Base Exception type for trakt module""" From 0eef8b1b6f13f8c422db0753e005f08b7a744418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:46:26 +0200 Subject: [PATCH 147/193] Export decorators for backward compat --- trakt/core.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/trakt/core.py b/trakt/core.py index 339a9972..1c110706 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -8,6 +8,8 @@ import requests +from trakt import decorators + __author__ = 'Jon Nappi' __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', @@ -93,3 +95,10 @@ def api(): client.set_auth(auth) return client + + +# Backward compat with 3.x +delete = decorators.delete +get = decorators.get +post = decorators.post +put = decorators.put From 961c25c112285e4ad6df6d42104fa16af351dc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:49:55 +0200 Subject: [PATCH 148/193] fixup! Export decorators for backward compat --- trakt/core.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/trakt/core.py b/trakt/core.py index 1c110706..65c679a5 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -8,8 +8,6 @@ import requests -from trakt import decorators - __author__ = 'Jon Nappi' __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', @@ -98,7 +96,21 @@ def api(): # Backward compat with 3.x -delete = decorators.delete -get = decorators.get -post = decorators.post -put = decorators.put +def delete(f): + from trakt.decorators import delete + return delete(f) + + +def get(f): + from trakt.decorators import get + return get(f) + + +def post(f): + from trakt.decorators import post + return post(f) + + +def put(f): + from trakt.decorators import put + return put(f) From 13089002df8b20c6ef7505a80c765fdce47bd47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Thu, 13 Jan 2022 23:56:05 +0200 Subject: [PATCH 149/193] Partial update to use AuthConfig for DeviceAuth --- trakt/auth/__init__.py | 4 ++-- trakt/auth/device.py | 34 ++++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index e805f02a..2f215c9e 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -3,7 +3,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' -from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH +from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH, api def pin_auth(*args, **kwargs): @@ -21,7 +21,7 @@ def oauth_auth(*args, **kwargs): def device_auth(*args, **kwargs): from trakt.auth.device import DeviceAuth - return DeviceAuth(*args, **kwargs).authenticate() + return DeviceAuth(*args, client=api(), **kwargs).authenticate() def get_client_info(app_id=False): diff --git a/trakt/auth/device.py b/trakt/auth/device.py index bab54345..3f30c0ed 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -1,10 +1,12 @@ import time -from trakt.auth import get_client_info, _store +from trakt.api import HttpClient +from trakt.auth import get_client_info +from trakt.config import AuthConfig class DeviceAuth: - def __init__(self, client_id=None, client_secret=None, store=False): + def __init__(self, client: HttpClient, config: AuthConfig, client_id=None, client_secret=None, store=False): """ :param client_id: Your Trakt OAuth Application's Client ID :param client_secret: Your Trakt OAuth Application's Client Secret @@ -12,6 +14,8 @@ def __init__(self, client_id=None, client_secret=None, store=False): should be stored locally on the system. Default is :const:`False` for the security conscious """ + self.client = client + self.config = config self.client_id = client_id self.client_secret = client_secret self.store = store @@ -77,10 +81,10 @@ def get_device_code(self, client_id=None, client_secret=None): :param client_secret: Your Trakt OAuth Application's Client Secret :return: Your OAuth device code. """ - global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN + if client_id is None and client_secret is None: client_id, client_secret = get_client_info() - CLIENT_ID, CLIENT_SECRET = client_id, client_secret + self.config.CLIENT_ID, self.config.CLIENT_SECRET = client_id, client_secret HEADERS['trakt-api-key'] = CLIENT_ID device_code_url = urljoin(BASE_URL, '/oauth/device/code') @@ -115,16 +119,16 @@ def get_device_token(self, device_code, client_id=None, client_secret=None, :return: Information regarding the authentication polling. :return type: dict """ - global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN, OAUTH_REFRESH + if client_id is None and client_secret is None: client_id, client_secret = get_client_info() - CLIENT_ID, CLIENT_SECRET = client_id, client_secret + self.config.CLIENT_ID, self.config.CLIENT_SECRET = client_id, client_secret HEADERS['trakt-api-key'] = CLIENT_ID data = { "code": device_code, - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET + "client_id": self.config.CLIENT_ID, + "client_secret": self.config.CLIENT_SECRET } response = session.post( @@ -134,15 +138,17 @@ def get_device_token(self, device_code, client_id=None, client_secret=None, # We only get json on success. if response.status_code == 200: data = response.json() - OAUTH_TOKEN = data.get('access_token') - OAUTH_REFRESH = data.get('refresh_token') - OAUTH_EXPIRES_AT = data.get("created_at") + data.get("expires_in") + self.config.OAUTH_TOKEN = data.get('access_token') + self.config.OAUTH_REFRESH = data.get('refresh_token') + self.config.OAUTH_EXPIRES_AT = data.get("created_at") + data.get("expires_in") if store: _store( - CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, - OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT + CLIENT_ID=self.config.CLIENT_ID, + CLIENT_SECRET=self.config.CLIENT_SECRET, + OAUTH_TOKEN=self.config.OAUTH_TOKEN, + OAUTH_REFRESH=self.config.OAUTH_REFRESH, + OAUTH_EXPIRES_AT=self.config.OAUTH_EXPIRES_AT, ) return response From 9e165f16547c94d1e88b793e6e3721c7fe37b5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:05:05 +0200 Subject: [PATCH 150/193] Optional headers support for post (maybe not needed) --- trakt/api.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 20f4b6b2..56d55b44 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -3,7 +3,7 @@ import json import logging from datetime import datetime, timedelta, timezone -from functools import lru_cache +from functools import lru_cache, partial from requests import Session from requests.auth import AuthBase @@ -35,8 +35,8 @@ def get(self, url: str): def delete(self, url: str): self.request('delete', url) - def post(self, url: str, data): - return self.request('post', url, data=data) + def post(self, url: str, data, headers=None): + return self.request('post', url, data=data, headers=None) def put(self, url: str, data): return self.request('put', url, data=data) @@ -44,7 +44,7 @@ def put(self, url: str, data): def set_auth(self, auth): self.auth = auth - def request(self, method, url, data=None): + def request(self, method, url, data=None, headers=None): """Handle actually talking out to the trakt API, logging out debug information, raising any relevant `TraktException` Exception types, and extracting and returning JSON data @@ -53,6 +53,7 @@ def request(self, method, url, data=None): post, put, delete, get :param url: The fully qualified url to send our request to :param data: Optional data payload to send to the API + :param headers: Optional headers for this request only :return: The decoded JSON response from the Trakt API :raises TraktException: If any non-200 return code is encountered """ @@ -60,10 +61,14 @@ def request(self, method, url, data=None): url = self.base_url + url self.logger.debug('%s: %s', method, url) self.logger.debug('method, url :: %s, %s', method, url) + + headers = self.headers.copy().update(headers) if headers else self.headers + request = partial(self.session.request, method, url, auth=self.auth, headers=headers) + if method == 'get': # GETs need to pass data as params, not body - response = self.session.request(method, url, headers=self.headers, auth=self.auth, params=data) + response = request(params=data) else: - response = self.session.request(method, url, headers=self.headers, auth=self.auth, data=json.dumps(data)) + response = request(data=json.dumps(data)) self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) if response.status_code == 204: # HTTP no content return None @@ -170,4 +175,3 @@ def refresh_token(self): ) ) self.config.store() - From 6e60a426e3cc9e1094af1bf51d0d404c7299b99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:06:22 +0200 Subject: [PATCH 151/193] Use HttpClient for get_device_code --- trakt/auth/device.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 3f30c0ed..1af6e4ec 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -85,14 +85,10 @@ def get_device_code(self, client_id=None, client_secret=None): if client_id is None and client_secret is None: client_id, client_secret = get_client_info() self.config.CLIENT_ID, self.config.CLIENT_SECRET = client_id, client_secret - HEADERS['trakt-api-key'] = CLIENT_ID - device_code_url = urljoin(BASE_URL, '/oauth/device/code') - headers = {'Content-Type': 'application/json'} - data = {"client_id": CLIENT_ID} + data = {"client_id": self.config.CLIENT_ID} + device_response = self.client.post('/oauth/device/code', data=data) - device_response = session.post(device_code_url, - json=data, headers=headers).json() print('Your user code is: {user_code}, please navigate to ' '{verification_url} to authenticate'.format( user_code=device_response.get('user_code'), From 49fc9b3e0110f5d04c30b64d0eb5ba32ec445780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:10:54 +0200 Subject: [PATCH 152/193] Extract common token update logic --- trakt/auth/device.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 1af6e4ec..89ef9c8b 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -49,13 +49,14 @@ def authenticate(self): "With access_token {access_token} and refresh_token {refresh_token}" ) - response = self.get_device_code(client_id=self.client_id, client_secret=self.client_secret) + self.update_tokens() + response = self.get_device_code() device_code = response['device_code'] interval = response['interval'] # No need to check for expiration, the API will notify us. while True: - response = self.get_device_token(device_code, self.client_id, self.client_secret, self.store) + response = self.get_device_token(device_code, self.store) if response.status_code == 200: print(success_message.format_map(response.json())) @@ -72,20 +73,14 @@ def authenticate(self): return response - def get_device_code(self, client_id=None, client_secret=None): + def get_device_code(self): """Generate a device code, used for device oauth authentication. Trakt docs: https://trakt.docs.apiary.io/#reference/ authentication-devices/device-code - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret :return: Your OAuth device code. """ - if client_id is None and client_secret is None: - client_id, client_secret = get_client_info() - self.config.CLIENT_ID, self.config.CLIENT_SECRET = client_id, client_secret - data = {"client_id": self.config.CLIENT_ID} device_response = self.client.post('/oauth/device/code', data=data) @@ -98,8 +93,7 @@ def get_device_code(self, client_id=None, client_secret=None): device_response['requested'] = time.time() return device_response - def get_device_token(self, device_code, client_id=None, client_secret=None, - store=False): + def get_device_token(self, device_code, store=False): """ Trakt docs: https://trakt.docs.apiary.io/#reference/ authentication-devices/get-token @@ -116,9 +110,6 @@ def get_device_token(self, device_code, client_id=None, client_secret=None, :return type: dict """ - if client_id is None and client_secret is None: - client_id, client_secret = get_client_info() - self.config.CLIENT_ID, self.config.CLIENT_SECRET = client_id, client_secret HEADERS['trakt-api-key'] = CLIENT_ID data = { @@ -148,3 +139,11 @@ def get_device_token(self, device_code, client_id=None, client_secret=None, ) return response + + def update_tokens(self): + """ + Update client_id, client_secret from input or ask them interactively + """ + if self.client_id is None and self.client_secret is None: + self.client_id, self.client_secret = get_client_info() + self.config.CLIENT_ID, self.config.CLIENT_SECRET = self.client_id, self.client_secret From c95eeeb01f8414687edf34bdbe1cc89d07b9d29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:11:18 +0200 Subject: [PATCH 153/193] Style fix --- trakt/auth/device.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 89ef9c8b..00e0828b 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -84,8 +84,7 @@ def get_device_code(self): data = {"client_id": self.config.CLIENT_ID} device_response = self.client.post('/oauth/device/code', data=data) - print('Your user code is: {user_code}, please navigate to ' - '{verification_url} to authenticate'.format( + print('Your user code is: {user_code}, please navigate to {verification_url} to authenticate'.format( user_code=device_response.get('user_code'), verification_url=device_response.get('verification_url') )) From f5e7d2ce3addbc9adb26e8c616298ad4bc26edea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:13:02 +0200 Subject: [PATCH 154/193] Simplified import --- trakt/auth/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 00e0828b..18d5b606 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -1,4 +1,4 @@ -import time +from time import sleep, time from trakt.api import HttpClient from trakt.auth import get_client_info @@ -69,7 +69,7 @@ def authenticate(self): print(error_messages.get(response.status_code, response.reason)) break - time.sleep(interval) + sleep(interval) return response @@ -89,7 +89,7 @@ def get_device_code(self): verification_url=device_response.get('verification_url') )) - device_response['requested'] = time.time() + device_response['requested'] = time() return device_response def get_device_token(self, device_code, store=False): From ed9af4c96293bb7d39c62299864dd9aac9a88f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:13:11 +0200 Subject: [PATCH 155/193] Variable cosmetic --- trakt/auth/device.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 18d5b606..04a40cb4 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -82,15 +82,16 @@ def get_device_code(self): """ data = {"client_id": self.config.CLIENT_ID} - device_response = self.client.post('/oauth/device/code', data=data) + response = self.client.post('/oauth/device/code', data=data) print('Your user code is: {user_code}, please navigate to {verification_url} to authenticate'.format( - user_code=device_response.get('user_code'), - verification_url=device_response.get('verification_url') + user_code=response.get('user_code'), + verification_url=response.get('verification_url') )) - device_response['requested'] = time() - return device_response + response['requested'] = time() + + return response def get_device_token(self, device_code, store=False): """ From 1b6e0b3bd363dfded3d27c4046a7033a86fc1831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:14:33 +0200 Subject: [PATCH 156/193] Use HttpClient in get_device_token --- trakt/auth/device.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 04a40cb4..383c632a 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -110,17 +110,12 @@ def get_device_token(self, device_code, store=False): :return type: dict """ - HEADERS['trakt-api-key'] = CLIENT_ID - data = { "code": device_code, "client_id": self.config.CLIENT_ID, "client_secret": self.config.CLIENT_SECRET } - - response = session.post( - urljoin(BASE_URL, '/oauth/device/token'), json=data - ) + response = self.client.post('/oauth/device/token', data=data) # We only get json on success. if response.status_code == 200: From a417b786e4f1a998c82e88f4d23c36a225fa7ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:15:11 +0200 Subject: [PATCH 157/193] Bulk update config --- trakt/auth/device.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 383c632a..7bc2a430 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -120,9 +120,11 @@ def get_device_token(self, device_code, store=False): # We only get json on success. if response.status_code == 200: data = response.json() - self.config.OAUTH_TOKEN = data.get('access_token') - self.config.OAUTH_REFRESH = data.get('refresh_token') - self.config.OAUTH_EXPIRES_AT = data.get("created_at") + data.get("expires_in") + self.config.update( + OAUTH_TOKEN=data.get('access_token'), + OAUTH_REFRESH=data.get('refresh_token'), + OAUTH_EXPIRES_AT=data.get("created_at") + data.get("expires_in"), + ) if store: _store( From 6854cb2de49643180c82be8b620dca8f8bda4d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:15:40 +0200 Subject: [PATCH 158/193] use self.config.store --- trakt/auth/device.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 7bc2a430..46907cd3 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -127,13 +127,7 @@ def get_device_token(self, device_code, store=False): ) if store: - _store( - CLIENT_ID=self.config.CLIENT_ID, - CLIENT_SECRET=self.config.CLIENT_SECRET, - OAUTH_TOKEN=self.config.OAUTH_TOKEN, - OAUTH_REFRESH=self.config.OAUTH_REFRESH, - OAUTH_EXPIRES_AT=self.config.OAUTH_EXPIRES_AT, - ) + self.config.store() return response From abb3274daa4870411190e6c3864cef8144c774e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:16:21 +0200 Subject: [PATCH 159/193] Fix DeviceAuth factory --- trakt/auth/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 2f215c9e..b4da8e0c 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -3,7 +3,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' -from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH, api +from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH, api, config def pin_auth(*args, **kwargs): @@ -21,7 +21,7 @@ def oauth_auth(*args, **kwargs): def device_auth(*args, **kwargs): from trakt.auth.device import DeviceAuth - return DeviceAuth(*args, client=api(), **kwargs).authenticate() + return DeviceAuth(*args, client=api(), config=config(), **kwargs).authenticate() def get_client_info(app_id=False): From b13e65455e1dad6ff87479f81f116e20da654f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:27:11 +0200 Subject: [PATCH 160/193] Use exceptions --- trakt/auth/device.py | 47 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 46907cd3..d54168ff 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -3,6 +3,7 @@ from trakt.api import HttpClient from trakt.auth import get_client_info from trakt.config import AuthConfig +from trakt.errors import TraktException, RateLimitException, BadRequestException class DeviceAuth: @@ -56,23 +57,24 @@ def authenticate(self): # No need to check for expiration, the API will notify us. while True: - response = self.get_device_token(device_code, self.store) - - if response.status_code == 200: + try: + response = self.get_device_token(device_code, self.store) print(success_message.format_map(response.json())) - break - - elif response.status_code == 429: # slow down + return response + except RateLimitException: + # slow down interval *= 2 - - elif response.status_code != 400: # not pending - print(error_messages.get(response.status_code, response.reason)) - break + except BadRequestException as e: + # XXX? what now? + # # elif response.status_code != 400: # not pending + # raise e + print(e) + return None + except TraktException as e: + print(error_messages.get(e.http_code, response.response)) sleep(interval) - return response - def get_device_code(self): """Generate a device code, used for device oauth authentication. @@ -115,19 +117,18 @@ def get_device_token(self, device_code, store=False): "client_id": self.config.CLIENT_ID, "client_secret": self.config.CLIENT_SECRET } + + # We only get json on success. Code throws on errors response = self.client.post('/oauth/device/token', data=data) - # We only get json on success. - if response.status_code == 200: - data = response.json() - self.config.update( - OAUTH_TOKEN=data.get('access_token'), - OAUTH_REFRESH=data.get('refresh_token'), - OAUTH_EXPIRES_AT=data.get("created_at") + data.get("expires_in"), - ) - - if store: - self.config.store() + self.config.update( + OAUTH_TOKEN=response.get('access_token'), + OAUTH_REFRESH=response.get('refresh_token'), + OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"), + ) + + if store: + self.config.store() return response From f5046abca750afc3dffe5527744529d1ff3b2a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 00:28:02 +0200 Subject: [PATCH 161/193] Move mappings to class properties --- trakt/auth/device.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index d54168ff..b2cbaddb 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -7,6 +7,18 @@ class DeviceAuth: + error_messages = { + 404: 'Invalid device_code', + 409: 'You already approved this code', + 410: 'The tokens have expired, restart the process', + 418: 'You explicitly denied this code', + } + + success_message = ( + "You've been successfully authenticated. " + "With access_token {access_token} and refresh_token {refresh_token}" + ) + def __init__(self, client: HttpClient, config: AuthConfig, client_id=None, client_secret=None, store=False): """ :param client_id: Your Trakt OAuth Application's Client ID @@ -38,17 +50,6 @@ def authenticate(self): :return: A dict with the authentication result. Or False of authentication failed. """ - error_messages = { - 404: 'Invalid device_code', - 409: 'You already approved this code', - 410: 'The tokens have expired, restart the process', - 418: 'You explicitly denied this code', - } - - success_message = ( - "You've been successfully authenticated. " - "With access_token {access_token} and refresh_token {refresh_token}" - ) self.update_tokens() response = self.get_device_code() @@ -59,7 +60,7 @@ def authenticate(self): while True: try: response = self.get_device_token(device_code, self.store) - print(success_message.format_map(response.json())) + print(self.success_message.format_map(response.json())) return response except RateLimitException: # slow down @@ -71,7 +72,7 @@ def authenticate(self): print(e) return None except TraktException as e: - print(error_messages.get(e.http_code, response.response)) + print(self.error_messages.get(e.http_code, response.response)) sleep(interval) From 8d439722caffa36190d4d966c158f2b2b46fed10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:22:54 +0200 Subject: [PATCH 162/193] Update OAuth to work --- trakt/auth/__init__.py | 2 +- trakt/auth/oauth.py | 54 ++++++++++++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index b4da8e0c..ca7f42fd 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -15,7 +15,7 @@ def pin_auth(*args, **kwargs): def oauth_auth(*args, **kwargs): from trakt.auth.oauth import OAuth - return OAuth(*args, **kwargs).authenticate() + return OAuth(*args, client=api(), config=config(), **kwargs).authenticate() def device_auth(*args, **kwargs): diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index beb1c388..6040ee5f 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -1,9 +1,18 @@ -from trakt.auth import get_client_info, _store +from urllib.parse import urljoin + from requests_oauthlib import OAuth2Session +from trakt.api import HttpClient +from trakt.auth import get_client_info +from trakt.config import AuthConfig + class OAuth: - def __init__(self, username, client_id=None, client_secret=None, store=False, oauth_cb=None): + #: The OAuth2 Redirect URI for your OAuth Application + REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' + + def __init__(self, username, client: HttpClient, config: AuthConfig, client_id=None, client_secret=None, + store=False, oauth_cb=None): """ :param username: Your trakt.tv username :param client_id: Your Trakt OAuth Application's Client ID @@ -15,6 +24,8 @@ def __init__(self, username, client_id=None, client_secret=None, store=False, oa PIN. Default function `_terminal_oauth_pin` for terminal auth """ self.username = username + self.client = client + self.config = config self.client_id = client_id self.client_secret = client_secret self.store = store @@ -26,17 +37,15 @@ def authenticate(self): :return: Your OAuth access token """ - global CLIENT_ID, CLIENT_SECRET, OAUTH_TOKEN - if self.client_id is None and self.client_secret is None: - self.client_id, self.client_secret = get_client_info() - CLIENT_ID, CLIENT_SECRET = self.client_id, self.client_secret - HEADERS['trakt-api-key'] = CLIENT_ID - authorization_base_url = urljoin(BASE_URL, '/oauth/authorize') - token_url = urljoin(BASE_URL, '/oauth/token') + self.update_tokens() + + base_url = self.client.base_url + authorization_base_url = urljoin(base_url, '/oauth/authorize') + token_url = urljoin(base_url, '/oauth/token') # OAuth endpoints given in the API documentation - oauth = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI, state=None) + oauth = OAuth2Session(self.config.CLIENT_ID, redirect_uri=self.REDIRECT_URI, state=None) # Authorization URL to redirect user to Trakt for authorization authorization_url, _ = oauth.authorization_url(authorization_base_url, username=self.username) @@ -45,16 +54,25 @@ def authenticate(self): oauth_pin = self.oauth_cb(authorization_url) # Fetch, assign, and return the access token - oauth.fetch_token(token_url, client_secret=CLIENT_SECRET, code=oauth_pin) - OAUTH_TOKEN = oauth.token['access_token'] - OAUTH_REFRESH = oauth.token['refresh_token'] - OAUTH_EXPIRES_AT = oauth.token["created_at"] + oauth.token["expires_in"] + oauth.fetch_token(token_url, client_secret=self.config.CLIENT_SECRET, code=oauth_pin) + self.config.update( + OAUTH_TOKEN=oauth.token['access_token'], + OAUTH_REFRESH=oauth.token['refresh_token'], + OAUTH_EXPIRES_AT=oauth.token["created_at"] + oauth.token["expires_in"], + ) if self.store: - _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, OAUTH_REFRESH=OAUTH_REFRESH, - OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT) - return OAUTH_TOKEN + self.config.store() + + return self.config.OAUTH_TOKEN + + def update_tokens(self): + """ + Update client_id, client_secret from input or ask them interactively + """ + if self.client_id is None and self.client_secret is None: + self.client_id, self.client_secret = get_client_info() + self.config.CLIENT_ID, self.config.CLIENT_SECRET = self.client_id, self.client_secret @staticmethod def terminal_oauth_pin(authorization_url): From e1640e807e9dd1f992f9a2d367afa105ffe9fd31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:29:03 +0200 Subject: [PATCH 163/193] Update PinAuth to work --- trakt/auth/__init__.py | 2 +- trakt/auth/pin.py | 57 ++++++++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index ca7f42fd..d42d79eb 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -9,7 +9,7 @@ def pin_auth(*args, **kwargs): from trakt.auth.pin import PinAuth - return PinAuth(*args, **kwargs).authenticate() + return PinAuth(*args, client=api(), config=config(), **kwargs).authenticate() def oauth_auth(*args, **kwargs): diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 852229e4..85cdd5dd 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -1,8 +1,16 @@ -from trakt.auth import _store +import sys + +from trakt.api import HttpClient +from trakt.auth import get_client_info +from trakt.config import AuthConfig class PinAuth: - def __init__(self, pin=None, client_id=None, client_secret=None, store=False): + #: The OAuth2 Redirect URI for your OAuth Application + REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' + + def __init__(self, client: HttpClient, config: AuthConfig, pin=None, client_id=None, client_secret=None, + store=False): """ :param pin: Optional Trakt API PIN code. If one is not specified, you will be prompted to go generate one @@ -14,6 +22,8 @@ def __init__(self, pin=None, client_id=None, client_secret=None, store=False): the security conscious """ self.pin = pin + self.client = client + self.config = config self.client_id = client_id self.client_secret = client_secret self.store = store @@ -24,10 +34,8 @@ def authenticate(self): :return: Your OAuth access token """ - global OAUTH_TOKEN, CLIENT_ID, CLIENT_SECRET - CLIENT_ID, CLIENT_SECRET = self.client_id, self.client_secret - if self.client_id is None and self.client_secret is None: - CLIENT_ID, CLIENT_SECRET = _get_client_info(app_id=True) + self.update_tokens() + if self.pin is None and APPLICATION_ID is None: print('You must set the APPLICATION_ID of the Trakt application you ' 'wish to use. You can find this ID by visiting the following ' @@ -40,16 +48,33 @@ def authenticate(self): pin_url = 'https://trakt.tv/pin/{id}'.format(id=APPLICATION_ID) print(pin_url) self.pin = input('Please enter your PIN: ') - args = {'code': self.pin, - 'redirect_uri': REDIRECT_URI, - 'grant_type': 'authorization_code', - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET} + data = { + 'code': self.pin, + 'redirect_uri': self.REDIRECT_URI, + 'grant_type': 'authorization_code', + 'client_id': self.config.CLIENT_ID, + 'client_secret': self.config.CLIENT_SECRET, + } + + response = self.client.post('/oauth/token', data) + self.config.OAUTH_TOKEN = response.get('access_token', None) - response = session.post(''.join([BASE_URL, '/oauth/token']), data=args) - OAUTH_TOKEN = response.json().get('access_token', None) + # self.config.update( + # CLIENT_ID=CLIENT_ID, + # CLIENT_SECRET=CLIENT_SECRET, + # OAUTH_TOKEN=OAUTH_TOKEN, + # APPLICATION_ID=APPLICATION_ID + # ) if self.store: - _store(CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, - OAUTH_TOKEN=OAUTH_TOKEN, APPLICATION_ID=APPLICATION_ID) - return OAUTH_TOKEN + self.config.store() + + return self.config.OAUTH_TOKEN + + def update_tokens(self): + """ + Update client_id, client_secret from input or ask them interactively + """ + if self.client_id is None and self.client_secret is None: + self.client_id, self.client_secret = get_client_info() + self.config.CLIENT_ID, self.config.CLIENT_SECRET = self.client_id, self.client_secret From 3701b4a4ab96e431fac3a49d99f214626ef0ea87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:32:02 +0200 Subject: [PATCH 164/193] Remove "store" logic from adapters Handle the logic in common place --- trakt/auth/__init__.py | 19 +++++++++++-------- trakt/auth/device.py | 10 +++------- trakt/auth/oauth.py | 9 +-------- trakt/auth/pin.py | 7 +------ 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index d42d79eb..365153fb 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -45,14 +45,13 @@ def get_client_info(app_id=False): return client_id, client_secret -def _store(**kwargs): - from trakt.core import config +def init_auth(method: str, store=False, *args, **kwargs): + """Run the auth function specified by *AUTH_METHOD* - config().store(**kwargs) - - -def init_auth(method: str, *args, **kwargs): - """Run the auth function specified by *AUTH_METHOD*""" + :param store: Boolean flag used to determine if your trakt api auth data + should be stored locally on the system. Default is :const:`False` for + the security conscious + """ methods = { PIN_AUTH: pin_auth, @@ -60,4 +59,8 @@ def init_auth(method: str, *args, **kwargs): DEVICE_AUTH: device_auth, } - return methods.get(method, PIN_AUTH)(*args, **kwargs) + result = methods.get(method, PIN_AUTH)(*args, **kwargs) + if store: + config().store() + + return result diff --git a/trakt/auth/device.py b/trakt/auth/device.py index b2cbaddb..6c694b8b 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -19,7 +19,7 @@ class DeviceAuth: "With access_token {access_token} and refresh_token {refresh_token}" ) - def __init__(self, client: HttpClient, config: AuthConfig, client_id=None, client_secret=None, store=False): + def __init__(self, client: HttpClient, config: AuthConfig, client_id=None, client_secret=None): """ :param client_id: Your Trakt OAuth Application's Client ID :param client_secret: Your Trakt OAuth Application's Client Secret @@ -31,7 +31,6 @@ def __init__(self, client: HttpClient, config: AuthConfig, client_id=None, clien self.config = config self.client_id = client_id self.client_secret = client_secret - self.store = store def authenticate(self): """Process for authenticating using device authentication. @@ -59,7 +58,7 @@ def authenticate(self): # No need to check for expiration, the API will notify us. while True: try: - response = self.get_device_token(device_code, self.store) + response = self.get_device_token(device_code) print(self.success_message.format_map(response.json())) return response except RateLimitException: @@ -96,7 +95,7 @@ def get_device_code(self): return response - def get_device_token(self, device_code, store=False): + def get_device_token(self, device_code): """ Trakt docs: https://trakt.docs.apiary.io/#reference/ authentication-devices/get-token @@ -128,9 +127,6 @@ def get_device_token(self, device_code, store=False): OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"), ) - if store: - self.config.store() - return response def update_tokens(self): diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 6040ee5f..20fd1db6 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -12,14 +12,11 @@ class OAuth: REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' def __init__(self, username, client: HttpClient, config: AuthConfig, client_id=None, client_secret=None, - store=False, oauth_cb=None): + oauth_cb=None): """ :param username: Your trakt.tv username :param client_id: Your Trakt OAuth Application's Client ID :param client_secret: Your Trakt OAuth Application's Client Secret - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious :param oauth_cb: Callback function to handle the retrieving of the OAuth PIN. Default function `_terminal_oauth_pin` for terminal auth """ @@ -28,7 +25,6 @@ def __init__(self, username, client: HttpClient, config: AuthConfig, client_id=N self.config = config self.client_id = client_id self.client_secret = client_secret - self.store = store self.oauth_cb = self.terminal_oauth_pin if oauth_cb is None else oauth_cb def authenticate(self): @@ -61,9 +57,6 @@ def authenticate(self): OAUTH_EXPIRES_AT=oauth.token["created_at"] + oauth.token["expires_in"], ) - if self.store: - self.config.store() - return self.config.OAUTH_TOKEN def update_tokens(self): diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 85cdd5dd..9b126e6a 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -9,8 +9,7 @@ class PinAuth: #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' - def __init__(self, client: HttpClient, config: AuthConfig, pin=None, client_id=None, client_secret=None, - store=False): + def __init__(self, client: HttpClient, config: AuthConfig, pin=None, client_id=None, client_secret=None): """ :param pin: Optional Trakt API PIN code. If one is not specified, you will be prompted to go generate one @@ -26,7 +25,6 @@ def __init__(self, client: HttpClient, config: AuthConfig, pin=None, client_id=N self.config = config self.client_id = client_id self.client_secret = client_secret - self.store = store def authenticate(self): """Generate an access_token from a Trakt API PIN code. @@ -66,9 +64,6 @@ def authenticate(self): # APPLICATION_ID=APPLICATION_ID # ) - if self.store: - self.config.store() - return self.config.OAUTH_TOKEN def update_tokens(self): From 1188a4f12e4dfdeab924d0ae6514ef0af9ffb640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:33:40 +0200 Subject: [PATCH 165/193] Rename auth classes with Adapter suffix --- trakt/auth/__init__.py | 12 ++++++------ trakt/auth/device.py | 2 +- trakt/auth/oauth.py | 2 +- trakt/auth/pin.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 365153fb..e4396023 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -7,21 +7,21 @@ def pin_auth(*args, **kwargs): - from trakt.auth.pin import PinAuth + from trakt.auth.pin import PinAuthAdapter - return PinAuth(*args, client=api(), config=config(), **kwargs).authenticate() + return PinAuthAdapter(*args, client=api(), config=config(), **kwargs).authenticate() def oauth_auth(*args, **kwargs): - from trakt.auth.oauth import OAuth + from trakt.auth.oauth import OAuthAdapter - return OAuth(*args, client=api(), config=config(), **kwargs).authenticate() + return OAuthAdapter(*args, client=api(), config=config(), **kwargs).authenticate() def device_auth(*args, **kwargs): - from trakt.auth.device import DeviceAuth + from trakt.auth.device import DeviceAuthAdapter - return DeviceAuth(*args, client=api(), config=config(), **kwargs).authenticate() + return DeviceAuthAdapter(*args, client=api(), config=config(), **kwargs).authenticate() def get_client_info(app_id=False): diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 6c694b8b..348a9697 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -6,7 +6,7 @@ from trakt.errors import TraktException, RateLimitException, BadRequestException -class DeviceAuth: +class DeviceAuthAdapter: error_messages = { 404: 'Invalid device_code', 409: 'You already approved this code', diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 20fd1db6..19e703d2 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -7,7 +7,7 @@ from trakt.config import AuthConfig -class OAuth: +class OAuthAdapter: #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 9b126e6a..b2d51452 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -5,7 +5,7 @@ from trakt.config import AuthConfig -class PinAuth: +class PinAuthAdapter: #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' From 16ada2c88f897ff8bd286f6b3657ef59b8f9c062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:37:48 +0200 Subject: [PATCH 166/193] fixup! Remove "store" logic from adapters --- trakt/auth/device.py | 3 --- trakt/auth/pin.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 348a9697..3af12711 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -23,9 +23,6 @@ def __init__(self, client: HttpClient, config: AuthConfig, client_id=None, clien """ :param client_id: Your Trakt OAuth Application's Client ID :param client_secret: Your Trakt OAuth Application's Client Secret - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious """ self.client = client self.config = config diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index b2d51452..ce3e917c 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -16,9 +16,6 @@ def __init__(self, client: HttpClient, config: AuthConfig, pin=None, client_id=N :param client_id: The oauth client_id for authenticating to the trakt API. :param client_secret: The oauth client_secret for authenticating to the trakt API. - :param store: Boolean flag used to determine if your trakt api auth data - should be stored locally on the system. Default is :const:`False` for - the security conscious """ self.pin = pin self.client = client From 0e407d2b8c90423b09228deacbafa18b21aa2017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:39:13 +0200 Subject: [PATCH 167/193] Remove client_id, client_secret from Adapter constructor They are filled into AuthConfig before adapters --- trakt/auth/__init__.py | 30 ++++++++++++++++++++---------- trakt/auth/device.py | 19 ++----------------- trakt/auth/oauth.py | 17 +---------------- trakt/auth/pin.py | 19 ++----------------- 4 files changed, 25 insertions(+), 60 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index e4396023..5f891c30 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -3,25 +3,25 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' -from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH, api, config +from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH, api, config as config_factory -def pin_auth(*args, **kwargs): +def pin_auth(*args, config, **kwargs): from trakt.auth.pin import PinAuthAdapter - return PinAuthAdapter(*args, client=api(), config=config(), **kwargs).authenticate() + return PinAuthAdapter(*args, client=api(), config=config, **kwargs).authenticate() -def oauth_auth(*args, **kwargs): +def oauth_auth(*args, config, **kwargs): from trakt.auth.oauth import OAuthAdapter - return OAuthAdapter(*args, client=api(), config=config(), **kwargs).authenticate() + return OAuthAdapter(*args, client=api(), config=config, **kwargs).authenticate() -def device_auth(*args, **kwargs): +def device_auth(*args, config, **kwargs): from trakt.auth.device import DeviceAuthAdapter - return DeviceAuthAdapter(*args, client=api(), config=config(), **kwargs).authenticate() + return DeviceAuthAdapter(*args, client=api(), config=config, **kwargs).authenticate() def get_client_info(app_id=False): @@ -45,7 +45,7 @@ def get_client_info(app_id=False): return client_id, client_secret -def init_auth(method: str, store=False, *args, **kwargs): +def init_auth(method: str, client_id=None, client_secret=None, store=False, *args, **kwargs): """Run the auth function specified by *AUTH_METHOD* :param store: Boolean flag used to determine if your trakt api auth data @@ -59,8 +59,18 @@ def init_auth(method: str, store=False, *args, **kwargs): DEVICE_AUTH: device_auth, } - result = methods.get(method, PIN_AUTH)(*args, **kwargs) + config = config_factory() + + """ + Update client_id, client_secret from input or ask them interactively + """ + if client_id is None and client_secret is None: + client_id, client_secret = get_client_info() + config.CLIENT_ID, config.CLIENT_SECRET = client_id, client_secret + + adapter = methods.get(method, PIN_AUTH) + result = adapter(*args, config=config, **kwargs) if store: - config().store() + config.store() return result diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 3af12711..158d4eb5 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -19,15 +19,9 @@ class DeviceAuthAdapter: "With access_token {access_token} and refresh_token {refresh_token}" ) - def __init__(self, client: HttpClient, config: AuthConfig, client_id=None, client_secret=None): - """ - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret - """ + def __init__(self, client: HttpClient, config: AuthConfig): self.client = client self.config = config - self.client_id = client_id - self.client_secret = client_secret def authenticate(self): """Process for authenticating using device authentication. @@ -47,7 +41,6 @@ def authenticate(self): Or False of authentication failed. """ - self.update_tokens() response = self.get_device_code() device_code = response['device_code'] interval = response['interval'] @@ -124,12 +117,4 @@ def get_device_token(self, device_code): OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"), ) - return response - - def update_tokens(self): - """ - Update client_id, client_secret from input or ask them interactively - """ - if self.client_id is None and self.client_secret is None: - self.client_id, self.client_secret = get_client_info() - self.config.CLIENT_ID, self.config.CLIENT_SECRET = self.client_id, self.client_secret + return response \ No newline at end of file diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 19e703d2..61fd2976 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -11,20 +11,15 @@ class OAuthAdapter: #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' - def __init__(self, username, client: HttpClient, config: AuthConfig, client_id=None, client_secret=None, - oauth_cb=None): + def __init__(self, username, client: HttpClient, config: AuthConfig, oauth_cb=None): """ :param username: Your trakt.tv username - :param client_id: Your Trakt OAuth Application's Client ID - :param client_secret: Your Trakt OAuth Application's Client Secret :param oauth_cb: Callback function to handle the retrieving of the OAuth PIN. Default function `_terminal_oauth_pin` for terminal auth """ self.username = username self.client = client self.config = config - self.client_id = client_id - self.client_secret = client_secret self.oauth_cb = self.terminal_oauth_pin if oauth_cb is None else oauth_cb def authenticate(self): @@ -34,8 +29,6 @@ def authenticate(self): :return: Your OAuth access token """ - self.update_tokens() - base_url = self.client.base_url authorization_base_url = urljoin(base_url, '/oauth/authorize') token_url = urljoin(base_url, '/oauth/token') @@ -59,14 +52,6 @@ def authenticate(self): return self.config.OAUTH_TOKEN - def update_tokens(self): - """ - Update client_id, client_secret from input or ask them interactively - """ - if self.client_id is None and self.client_secret is None: - self.client_id, self.client_secret = get_client_info() - self.config.CLIENT_ID, self.config.CLIENT_SECRET = self.client_id, self.client_secret - @staticmethod def terminal_oauth_pin(authorization_url): """Default OAuth callback used for terminal applications. diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index ce3e917c..21d85e34 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -9,19 +9,14 @@ class PinAuthAdapter: #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' - def __init__(self, client: HttpClient, config: AuthConfig, pin=None, client_id=None, client_secret=None): + def __init__(self, client: HttpClient, config: AuthConfig, pin=None): """ :param pin: Optional Trakt API PIN code. If one is not specified, you will be prompted to go generate one - :param client_id: The oauth client_id for authenticating to the trakt API. - :param client_secret: The oauth client_secret for authenticating to the - trakt API. """ self.pin = pin self.client = client self.config = config - self.client_id = client_id - self.client_secret = client_secret def authenticate(self): """Generate an access_token from a Trakt API PIN code. @@ -29,8 +24,6 @@ def authenticate(self): :return: Your OAuth access token """ - self.update_tokens() - if self.pin is None and APPLICATION_ID is None: print('You must set the APPLICATION_ID of the Trakt application you ' 'wish to use. You can find this ID by visiting the following ' @@ -61,12 +54,4 @@ def authenticate(self): # APPLICATION_ID=APPLICATION_ID # ) - return self.config.OAUTH_TOKEN - - def update_tokens(self): - """ - Update client_id, client_secret from input or ask them interactively - """ - if self.client_id is None and self.client_secret is None: - self.client_id, self.client_secret = get_client_info() - self.config.CLIENT_ID, self.config.CLIENT_SECRET = self.client_id, self.client_secret + return self.config.OAUTH_TOKEN \ No newline at end of file From 9a2074cb31ae88f96fff39100a4d4416ec133755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:39:51 +0200 Subject: [PATCH 168/193] fixup! Remove client_id, client_secret from Adapter constructor --- trakt/auth/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 158d4eb5..5bf35244 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -117,4 +117,4 @@ def get_device_token(self, device_code): OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"), ) - return response \ No newline at end of file + return response From ef0859015e1e8b54db42ca6833df7eca0dfbe80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:40:09 +0200 Subject: [PATCH 169/193] fixup! fixup! Remove client_id, client_secret from Adapter constructor --- trakt/auth/device.py | 1 - trakt/auth/oauth.py | 1 - trakt/auth/pin.py | 1 - 3 files changed, 3 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 5bf35244..239937c8 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -1,7 +1,6 @@ from time import sleep, time from trakt.api import HttpClient -from trakt.auth import get_client_info from trakt.config import AuthConfig from trakt.errors import TraktException, RateLimitException, BadRequestException diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 61fd2976..fc180b68 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -3,7 +3,6 @@ from requests_oauthlib import OAuth2Session from trakt.api import HttpClient -from trakt.auth import get_client_info from trakt.config import AuthConfig diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 21d85e34..6188a3b3 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -1,7 +1,6 @@ import sys from trakt.api import HttpClient -from trakt.auth import get_client_info from trakt.config import AuthConfig From 09220aa80479ba4aafefaf2b1ae7ea113048c18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 17:42:48 +0200 Subject: [PATCH 170/193] DeviceAuthAdapter has no other arguments --- trakt/auth/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 5f891c30..092e359b 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -18,10 +18,10 @@ def oauth_auth(*args, config, **kwargs): return OAuthAdapter(*args, client=api(), config=config, **kwargs).authenticate() -def device_auth(*args, config, **kwargs): +def device_auth(config): from trakt.auth.device import DeviceAuthAdapter - return DeviceAuthAdapter(*args, client=api(), config=config, **kwargs).authenticate() + return DeviceAuthAdapter(client=api(), config=config).authenticate() def get_client_info(app_id=False): From 17e790a59e2ba437dc8df9b37c4ba6353fef7279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:06:00 +0200 Subject: [PATCH 171/193] Add APPLICATION_ID to AuthConfig --- trakt/auth/pin.py | 4 ++-- trakt/config.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 6188a3b3..4b618220 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -23,7 +23,7 @@ def authenticate(self): :return: Your OAuth access token """ - if self.pin is None and APPLICATION_ID is None: + if self.pin is None and self.config.APPLICATION_ID is None: print('You must set the APPLICATION_ID of the Trakt application you ' 'wish to use. You can find this ID by visiting the following ' 'URL.') @@ -32,7 +32,7 @@ def authenticate(self): if self.pin is None: print('If you do not have a Trakt.tv PIN, please visit the following ' 'url and log in to generate one.') - pin_url = 'https://trakt.tv/pin/{id}'.format(id=APPLICATION_ID) + pin_url = 'https://trakt.tv/pin/{id}'.format(id=self.config.APPLICATION_ID) print(pin_url) self.pin = input('Please enter your PIN: ') data = { diff --git a/trakt/config.py b/trakt/config.py index 6415297e..a0e5be77 100644 --- a/trakt/config.py +++ b/trakt/config.py @@ -11,6 +11,7 @@ @dataclass class AuthConfig: + APPLICATION_ID: Optional[str] CLIENT_ID: Optional[str] CLIENT_SECRET: Optional[str] OAUTH_EXPIRES_AT: Optional[int] From 1479769c4f95d07ff8a849877fb9e9b3c3235859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:16:02 +0200 Subject: [PATCH 172/193] Guard TokenAuth to skip oauth requests --- trakt/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/trakt/api.py b/trakt/api.py index 56d55b44..7e32037a 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -109,6 +109,10 @@ def __init__(self, client: HttpClient, config: AuthConfig): self.logger = logging.getLogger('trakt.api.token_auth') def __call__(self, r): + # Skip oauth requests + if r.url.startswith(f'{self.client.base_url}/oauth/'): + return r + [client_id, client_token] = self.get_token() r.headers.update({ From b5d93cdbd78a115027ce779945526ab289bf1db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:18:11 +0200 Subject: [PATCH 173/193] fixup! Guard TokenAuth to skip oauth requests --- trakt/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 7e32037a..6185076b 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -107,10 +107,11 @@ def __init__(self, client: HttpClient, config: AuthConfig): self.config = config self.client = client self.logger = logging.getLogger('trakt.api.token_auth') + self.base_url = f'{self.client.base_url.rstrip("/")}/oauth/' def __call__(self, r): # Skip oauth requests - if r.url.startswith(f'{self.client.base_url}/oauth/'): + if r.url.startswith(self.base_url): return r [client_id, client_token] = self.get_token() From 5c384a6156323d0386ae6f30137af982e1e4bbb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:18:47 +0200 Subject: [PATCH 174/193] fixup! fixup! Guard TokenAuth to skip oauth requests --- trakt/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 6185076b..f78f4667 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -107,11 +107,10 @@ def __init__(self, client: HttpClient, config: AuthConfig): self.config = config self.client = client self.logger = logging.getLogger('trakt.api.token_auth') - self.base_url = f'{self.client.base_url.rstrip("/")}/oauth/' def __call__(self, r): # Skip oauth requests - if r.url.startswith(self.base_url): + if r.path_url.startswith('/oauth/'): return r [client_id, client_token] = self.get_token() From caf5d0a86dad5a7bb82ee971e8742b0d321bb2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:26:32 +0200 Subject: [PATCH 175/193] Cleanup --- trakt/auth/pin.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 4b618220..58d68075 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -46,11 +46,4 @@ def authenticate(self): response = self.client.post('/oauth/token', data) self.config.OAUTH_TOKEN = response.get('access_token', None) - # self.config.update( - # CLIENT_ID=CLIENT_ID, - # CLIENT_SECRET=CLIENT_SECRET, - # OAUTH_TOKEN=OAUTH_TOKEN, - # APPLICATION_ID=APPLICATION_ID - # ) - - return self.config.OAUTH_TOKEN \ No newline at end of file + return self.config.OAUTH_TOKEN From 97a8541052429b5a94a2702ca827f4bc42a1e836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:31:53 +0200 Subject: [PATCH 176/193] Change auth init not to return anything it's not consistent, and not useful see https://github.com/moogar0880/PyTrakt/pull/178#issuecomment-1013356548 --- trakt/auth/__init__.py | 5 ++--- trakt/auth/device.py | 7 +------ trakt/auth/oauth.py | 4 ---- trakt/auth/pin.py | 4 ---- 4 files changed, 3 insertions(+), 17 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 092e359b..617c198b 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -69,8 +69,7 @@ def init_auth(method: str, client_id=None, client_secret=None, store=False, *arg config.CLIENT_ID, config.CLIENT_SECRET = client_id, client_secret adapter = methods.get(method, PIN_AUTH) - result = adapter(*args, config=config, **kwargs) + adapter(*args, config=config, **kwargs) + if store: config.store() - - return result diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 239937c8..38482f73 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -35,9 +35,6 @@ def authenticate(self): get_device_code and get_device_token. Where poll_for_device_token will check if the "offline" authentication was successful. - - :return: A dict with the authentication result. - Or False of authentication failed. """ response = self.get_device_code() @@ -114,6 +111,4 @@ def get_device_token(self, device_code): OAUTH_TOKEN=response.get('access_token'), OAUTH_REFRESH=response.get('refresh_token'), OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"), - ) - - return response + ) \ No newline at end of file diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index fc180b68..53dc132e 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -24,8 +24,6 @@ def __init__(self, username, client: HttpClient, config: AuthConfig, oauth_cb=No def authenticate(self): """Generate an access_token to allow your application to authenticate via OAuth - - :return: Your OAuth access token """ base_url = self.client.base_url @@ -49,8 +47,6 @@ def authenticate(self): OAUTH_EXPIRES_AT=oauth.token["created_at"] + oauth.token["expires_in"], ) - return self.config.OAUTH_TOKEN - @staticmethod def terminal_oauth_pin(authorization_url): """Default OAuth callback used for terminal applications. diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 58d68075..3c32de5b 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -19,8 +19,6 @@ def __init__(self, client: HttpClient, config: AuthConfig, pin=None): def authenticate(self): """Generate an access_token from a Trakt API PIN code. - - :return: Your OAuth access token """ if self.pin is None and self.config.APPLICATION_ID is None: @@ -45,5 +43,3 @@ def authenticate(self): response = self.client.post('/oauth/token', data) self.config.OAUTH_TOKEN = response.get('access_token', None) - - return self.config.OAUTH_TOKEN From 5a270d68156ca01e852e2c56b7d86c9a622ba383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:33:05 +0200 Subject: [PATCH 177/193] fixup! Change auth init not to return anything --- trakt/auth/device.py | 2 +- trakt/auth/oauth.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 38482f73..1b92162a 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -111,4 +111,4 @@ def get_device_token(self, device_code): OAUTH_TOKEN=response.get('access_token'), OAUTH_REFRESH=response.get('refresh_token'), OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"), - ) \ No newline at end of file + ) diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 53dc132e..27ce3eba 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -3,6 +3,7 @@ from requests_oauthlib import OAuth2Session from trakt.api import HttpClient + from trakt.config import AuthConfig From 89f25d008b2124fb730c43fa390497c4c1478c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:36:30 +0200 Subject: [PATCH 178/193] Shift named args --- trakt/auth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 617c198b..7ee0f855 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -45,7 +45,7 @@ def get_client_info(app_id=False): return client_id, client_secret -def init_auth(method: str, client_id=None, client_secret=None, store=False, *args, **kwargs): +def init_auth(method: str, *args, client_id=None, client_secret=None, store=False, **kwargs): """Run the auth function specified by *AUTH_METHOD* :param store: Boolean flag used to determine if your trakt api auth data From 970b41cb02f71ecb352b9176ee9ca09bbaabbe53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:43:06 +0200 Subject: [PATCH 179/193] Add BaseAdapter class --- trakt/auth/base.py | 2 ++ trakt/auth/device.py | 3 ++- trakt/auth/oauth.py | 3 ++- trakt/auth/pin.py | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 trakt/auth/base.py diff --git a/trakt/auth/base.py b/trakt/auth/base.py new file mode 100644 index 00000000..c2eea197 --- /dev/null +++ b/trakt/auth/base.py @@ -0,0 +1,2 @@ +class BaseAdapter: + pass diff --git a/trakt/auth/device.py b/trakt/auth/device.py index 1b92162a..fe24586e 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -1,11 +1,12 @@ from time import sleep, time from trakt.api import HttpClient +from trakt.auth.base import BaseAdapter from trakt.config import AuthConfig from trakt.errors import TraktException, RateLimitException, BadRequestException -class DeviceAuthAdapter: +class DeviceAuthAdapter(BaseAdapter): error_messages = { 404: 'Invalid device_code', 409: 'You already approved this code', diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index 27ce3eba..bd0e67e5 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -3,11 +3,12 @@ from requests_oauthlib import OAuth2Session from trakt.api import HttpClient +from trakt.auth.base import BaseAdapter from trakt.config import AuthConfig -class OAuthAdapter: +class OAuthAdapter(BaseAdapter): #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 3c32de5b..dbd72454 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -1,10 +1,11 @@ import sys from trakt.api import HttpClient +from trakt.auth.base import BaseAdapter from trakt.config import AuthConfig -class PinAuthAdapter: +class PinAuthAdapter(BaseAdapter): #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' From 806742ed3f406ae4b32a320ffeb16e1f4d28c5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:43:47 +0200 Subject: [PATCH 180/193] Move REDIRECT_URI to Base Adapter --- trakt/auth/base.py | 3 ++- trakt/auth/oauth.py | 3 --- trakt/auth/pin.py | 3 --- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/trakt/auth/base.py b/trakt/auth/base.py index c2eea197..56ce8d90 100644 --- a/trakt/auth/base.py +++ b/trakt/auth/base.py @@ -1,2 +1,3 @@ class BaseAdapter: - pass + #: The OAuth2 Redirect URI for your OAuth Application + REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' diff --git a/trakt/auth/oauth.py b/trakt/auth/oauth.py index bd0e67e5..3d76fddf 100644 --- a/trakt/auth/oauth.py +++ b/trakt/auth/oauth.py @@ -9,9 +9,6 @@ class OAuthAdapter(BaseAdapter): - #: The OAuth2 Redirect URI for your OAuth Application - REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' - def __init__(self, username, client: HttpClient, config: AuthConfig, oauth_cb=None): """ :param username: Your trakt.tv username diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index dbd72454..326edd1d 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -6,9 +6,6 @@ class PinAuthAdapter(BaseAdapter): - #: The OAuth2 Redirect URI for your OAuth Application - REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' - def __init__(self, client: HttpClient, config: AuthConfig, pin=None): """ :param pin: Optional Trakt API PIN code. If one is not specified, you will From f8bbc17aa624ee05362858f084cb4bc4c1e483a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:44:49 +0200 Subject: [PATCH 181/193] Add NEEDS_APPLICATION_ID to adapters --- trakt/auth/__init__.py | 2 +- trakt/auth/base.py | 3 +++ trakt/auth/pin.py | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 7ee0f855..4ed92840 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -60,6 +60,7 @@ def init_auth(method: str, *args, client_id=None, client_secret=None, store=Fals } config = config_factory() + adapter = methods.get(method, PIN_AUTH) """ Update client_id, client_secret from input or ask them interactively @@ -68,7 +69,6 @@ def init_auth(method: str, *args, client_id=None, client_secret=None, store=Fals client_id, client_secret = get_client_info() config.CLIENT_ID, config.CLIENT_SECRET = client_id, client_secret - adapter = methods.get(method, PIN_AUTH) adapter(*args, config=config, **kwargs) if store: diff --git a/trakt/auth/base.py b/trakt/auth/base.py index 56ce8d90..2ecca9d5 100644 --- a/trakt/auth/base.py +++ b/trakt/auth/base.py @@ -1,3 +1,6 @@ class BaseAdapter: #: The OAuth2 Redirect URI for your OAuth Application REDIRECT_URI: str = 'urn:ietf:wg:oauth:2.0:oob' + + #: True if the Adapter needs APPLICATION_ID + NEEDS_APPLICATION_ID = False diff --git a/trakt/auth/pin.py b/trakt/auth/pin.py index 326edd1d..fdd5c439 100644 --- a/trakt/auth/pin.py +++ b/trakt/auth/pin.py @@ -6,6 +6,8 @@ class PinAuthAdapter(BaseAdapter): + NEEDS_APPLICATION_ID = True + def __init__(self, client: HttpClient, config: AuthConfig, pin=None): """ :param pin: Optional Trakt API PIN code. If one is not specified, you will From 39dc7f83dd5b9c786bb08c79d26ffa4a61364c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:51:30 +0200 Subject: [PATCH 182/193] Update get_client_info to ask app id --- trakt/auth/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/trakt/auth/__init__.py b/trakt/auth/__init__.py index 4ed92840..00fdbeb9 100644 --- a/trakt/auth/__init__.py +++ b/trakt/auth/__init__.py @@ -4,6 +4,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' from trakt import PIN_AUTH, OAUTH_AUTH, DEVICE_AUTH, api, config as config_factory +from trakt.config import AuthConfig def pin_auth(*args, config, **kwargs): @@ -24,24 +25,22 @@ def device_auth(config): return DeviceAuthAdapter(client=api(), config=config).authenticate() -def get_client_info(app_id=False): +def get_client_info(app_id: bool, config: AuthConfig): """Helper function to poll the user for Client ID and Client Secret strings :return: A 2-tuple of client_id, client_secret """ - global APPLICATION_ID print('If you do not have a client ID and secret. Please visit the ' 'following url to create them.') print('http://trakt.tv/oauth/applications') client_id = input('Please enter your client id: ') client_secret = input('Please enter your client secret: ') if app_id: - msg = 'Please enter your application ID ({default}): '.format( - default=APPLICATION_ID) + msg = f'Please enter your application ID ({config.APPLICATION_ID}): ' user_input = input(msg) if user_input: - APPLICATION_ID = user_input + config.APPLICATION_ID = user_input return client_id, client_secret @@ -66,7 +65,7 @@ def init_auth(method: str, *args, client_id=None, client_secret=None, store=Fals Update client_id, client_secret from input or ask them interactively """ if client_id is None and client_secret is None: - client_id, client_secret = get_client_info() + client_id, client_secret = get_client_info(adapter.NEEDS_APPLICATION_ID, config) config.CLIENT_ID, config.CLIENT_SECRET = client_id, client_secret adapter(*args, config=config, **kwargs) From fb0c1480129bbf5181a043ac0b28ed0fe267c742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:54:56 +0200 Subject: [PATCH 183/193] Import global APPLICATION_ID --- trakt/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trakt/core.py b/trakt/core.py index 65c679a5..d4e766ef 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -76,6 +76,7 @@ def config(): from trakt.config import AuthConfig return AuthConfig(CONFIG_PATH).update( + APPLICATION_ID=APPLICATION_ID, CLIENT_ID=CLIENT_ID, CLIENT_SECRET=CLIENT_SECRET, OAUTH_EXPIRES_AT=OAUTH_EXPIRES_AT, From 7d9410f343dcfd0a100b270eec41a855816a23d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 14 Jan 2022 20:57:22 +0200 Subject: [PATCH 184/193] Drop todo --- trakt/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/trakt/core.py b/trakt/core.py index d4e766ef..34761355 100644 --- a/trakt/core.py +++ b/trakt/core.py @@ -9,6 +9,7 @@ import requests __author__ = 'Jon Nappi' +# XXX: drop? __all__ = ['Airs', 'Alias', 'Comment', 'Genre', 'init', 'BASE_URL', 'CLIENT_ID', 'CLIENT_SECRET', 'DEVICE_AUTH', 'CONFIG_PATH', 'OAUTH_TOKEN', @@ -29,12 +30,14 @@ CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.pytrakt.json') #: Your personal Trakt.tv OAUTH Bearer Token -OAUTH_TOKEN = api_key = None +OAUTH_TOKEN = None # Your OAUTH token expiration date +# XXX: drop? OAUTH_EXPIRES_AT = None # Your OAUTH refresh token +# XXX: drop? OAUTH_REFRESH = None #: Flag used to enable Trakt PIN authentication From 168c5a7c70c400e13e41ba605d4f50465c7d540a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 00:29:28 +0200 Subject: [PATCH 185/193] Fix BadRequestException handling. must continue waiting for code input --- trakt/auth/device.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index fe24586e..ed844753 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -51,12 +51,9 @@ def authenticate(self): except RateLimitException: # slow down interval *= 2 - except BadRequestException as e: - # XXX? what now? - # # elif response.status_code != 400: # not pending - # raise e - print(e) - return None + except BadRequestException: + # not pending + pass except TraktException as e: print(self.error_messages.get(e.http_code, response.response)) From 4e143b894ab5a82f104f1dc96a95779b57200bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 00:30:42 +0200 Subject: [PATCH 186/193] Fix success flow --- trakt/auth/device.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/trakt/auth/device.py b/trakt/auth/device.py index ed844753..b8a243ac 100644 --- a/trakt/auth/device.py +++ b/trakt/auth/device.py @@ -46,8 +46,8 @@ def authenticate(self): while True: try: response = self.get_device_token(device_code) - print(self.success_message.format_map(response.json())) - return response + print(self.success_message.format_map(response)) + break except RateLimitException: # slow down interval *= 2 @@ -110,3 +110,5 @@ def get_device_token(self, device_code): OAUTH_REFRESH=response.get('refresh_token'), OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"), ) + + return response From c13904955e8810cbb05922f609680388c7c9a9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 00:47:32 +0200 Subject: [PATCH 187/193] Revert "Optional headers support for post (maybe not needed)" This reverts commit 9e165f16547c94d1e88b793e6e3721c7fe37b5d1. --- trakt/api.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index f78f4667..468df76d 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -3,7 +3,7 @@ import json import logging from datetime import datetime, timedelta, timezone -from functools import lru_cache, partial +from functools import lru_cache from requests import Session from requests.auth import AuthBase @@ -35,8 +35,8 @@ def get(self, url: str): def delete(self, url: str): self.request('delete', url) - def post(self, url: str, data, headers=None): - return self.request('post', url, data=data, headers=None) + def post(self, url: str, data): + return self.request('post', url, data=data) def put(self, url: str, data): return self.request('put', url, data=data) @@ -44,7 +44,7 @@ def put(self, url: str, data): def set_auth(self, auth): self.auth = auth - def request(self, method, url, data=None, headers=None): + def request(self, method, url, data=None): """Handle actually talking out to the trakt API, logging out debug information, raising any relevant `TraktException` Exception types, and extracting and returning JSON data @@ -53,7 +53,6 @@ def request(self, method, url, data=None, headers=None): post, put, delete, get :param url: The fully qualified url to send our request to :param data: Optional data payload to send to the API - :param headers: Optional headers for this request only :return: The decoded JSON response from the Trakt API :raises TraktException: If any non-200 return code is encountered """ @@ -61,14 +60,10 @@ def request(self, method, url, data=None, headers=None): url = self.base_url + url self.logger.debug('%s: %s', method, url) self.logger.debug('method, url :: %s, %s', method, url) - - headers = self.headers.copy().update(headers) if headers else self.headers - request = partial(self.session.request, method, url, auth=self.auth, headers=headers) - if method == 'get': # GETs need to pass data as params, not body - response = request(params=data) + response = self.session.request(method, url, headers=self.headers, auth=self.auth, params=data) else: - response = request(data=json.dumps(data)) + response = self.session.request(method, url, headers=self.headers, auth=self.auth, data=json.dumps(data)) self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) if response.status_code == 204: # HTTP no content return None From 353623b44a6121e35cd8ba9df7ab5f524ba471cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 11:26:22 +0200 Subject: [PATCH 188/193] Remove duplicate logging --- trakt/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 468df76d..8c3aa4e2 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -59,7 +59,6 @@ def request(self, method, url, data=None): url = self.base_url + url self.logger.debug('%s: %s', method, url) - self.logger.debug('method, url :: %s, %s', method, url) if method == 'get': # GETs need to pass data as params, not body response = self.session.request(method, url, headers=self.headers, auth=self.auth, params=data) else: From e53aafa6457d96fd841d62920dcb63d72003c5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 11:27:11 +0200 Subject: [PATCH 189/193] Unify REQUEST/RESPONSE logging formatting --- trakt/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trakt/api.py b/trakt/api.py index 8c3aa4e2..91d7d6c8 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -58,7 +58,7 @@ def request(self, method, url, data=None): """ url = self.base_url + url - self.logger.debug('%s: %s', method, url) + self.logger.debug('REQUEST [%s] (%s)', method, url) if method == 'get': # GETs need to pass data as params, not body response = self.session.request(method, url, headers=self.headers, auth=self.auth, params=data) else: From 18cc0ead34a38a87ee278221d3679441915ba18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 11:31:10 +0200 Subject: [PATCH 190/193] Add BadResponseException exception with code -1 --- trakt/errors.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/trakt/errors.py b/trakt/errors.py index 8a54cd48..6c6a1acc 100644 --- a/trakt/errors.py +++ b/trakt/errors.py @@ -6,7 +6,13 @@ __author__ = 'Jon Nappi' __all__ = [ + # Base Exception 'TraktException', + + # Errors for use by PyTrakt + 'BadResponseException', + + # Exceptions by HTTP status code 'BadRequestException', 'OAuthException', 'ForbiddenException', @@ -32,6 +38,12 @@ def __str__(self): return self.message +class BadResponseException(TraktException): + """TraktException type to be raised when json could not be decoded""" + http_code = -1 + message = "Bad Response - Response could not be parsed" + + class BadRequestException(TraktException): """TraktException type to be raised when a 400 return code is received""" http_code = 400 From a60c8696a08f582092db9af7900913b3e022df76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 11:41:08 +0200 Subject: [PATCH 191/193] Catch json decode errors for unhandled status codes --- trakt/api.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 91d7d6c8..96349019 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -4,6 +4,7 @@ import logging from datetime import datetime, timedelta, timezone from functools import lru_cache +from json import JSONDecodeError from requests import Session from requests.auth import AuthBase @@ -13,7 +14,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' from trakt.config import AuthConfig -from trakt.errors import OAuthException +from trakt.errors import OAuthException, BadResponseException class HttpClient: @@ -64,11 +65,19 @@ def request(self, method, url, data=None): else: response = self.session.request(method, url, headers=self.headers, auth=self.auth, data=json.dumps(data)) self.logger.debug('RESPONSE [%s] (%s): %s', method, url, str(response)) + if response.status_code == 204: # HTTP no content return None self.raise_if_needed(response) - json_data = json.loads(response.content.decode('UTF-8', 'ignore')) - return json_data + + return self.decode_response(response) + + @staticmethod + def decode_response(response): + try: + return json.loads(response.content.decode('UTF-8', 'ignore')) + except JSONDecodeError as e: + raise BadResponseException(f"Unable to parse JSON: {e}") def raise_if_needed(self, response): if response.status_code in self.error_map: From c8865dc712fd70c0675c6328c10b994dd2e25489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 11:50:19 +0200 Subject: [PATCH 192/193] isort trakt/api.py --- trakt/api.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/trakt/api.py b/trakt/api.py index 96349019..7da8dc2e 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -10,12 +10,10 @@ from requests.auth import AuthBase from trakt import errors - -__author__ = 'Jon Nappi, Elan Ruusamäe' - from trakt.config import AuthConfig -from trakt.errors import OAuthException, BadResponseException +from trakt.errors import BadResponseException, OAuthException +__author__ = 'Jon Nappi, Elan Ruusamäe' class HttpClient: """Class for abstracting HTTP requests From e378190f548a1f363f822af0fca75773d210212d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 15 Jan 2022 11:52:01 +0200 Subject: [PATCH 193/193] PEP formatting --- trakt/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trakt/api.py b/trakt/api.py index 7da8dc2e..a9c8772b 100644 --- a/trakt/api.py +++ b/trakt/api.py @@ -15,6 +15,7 @@ __author__ = 'Jon Nappi, Elan Ruusamäe' + class HttpClient: """Class for abstracting HTTP requests """