From fb249e9843921fe4f93d279d2a0a7301d4e3fb08 Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Fri, 1 Dec 2023 18:04:59 +0000 Subject: [PATCH] feat: :sparkles: add debug mode Resolves: #73 --- semanticscholar/ApiRequester.py | 51 ++++++++++++++++++++++++- semanticscholar/AsyncSemanticScholar.py | 22 ++++++++++- semanticscholar/SemanticScholar.py | 23 ++++++++++- tests/data/debug_output.txt | 17 +++++++++ tests/data/test_debug.yaml | 50 ++++++++++++++++++++++++ tests/test_semanticscholar.py | 20 ++++++++++ 6 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 tests/data/debug_output.txt create mode 100644 tests/data/test_debug.yaml diff --git a/semanticscholar/ApiRequester.py b/semanticscholar/ApiRequester.py index f0fe589..bcc7cdf 100644 --- a/semanticscholar/ApiRequester.py +++ b/semanticscholar/ApiRequester.py @@ -3,6 +3,8 @@ import httpx import asyncio import warnings +import inspect +import json from tenacity import (retry, retry_if_exception_type, stop_after_attempt, wait_fixed) @@ -12,12 +14,14 @@ class ApiRequester: - def __init__(self, timeout) -> None: + def __init__(self, timeout, debug) -> None: ''' :param float timeout: an exception is raised \ if the server has not issued a response for timeout seconds. + :param bool debug: enable debug mode. ''' self.timeout = timeout + self.debug = debug @property def timeout(self) -> int: @@ -32,6 +36,48 @@ def timeout(self, timeout: int) -> None: :param int timeout: ''' self._timeout = timeout + + @property + def debug(self) -> bool: + ''' + :type: :class:`bool` + ''' + return self._debug + + @debug.setter + def debug(self, debug: bool) -> None: + ''' + :param bool debug: + ''' + self._debug = debug + + def _curl_cmd(self, url: str, method: str, headers: dict, payload: dict = None) -> str: + curl_cmd = f'curl -X {method}' + for key, value in headers.items(): + curl_cmd += f' -H \'{key}: {value}\'' + curl_cmd += f' -d \'{json.dumps(payload)}\'' if payload else '' + curl_cmd += f' {url}' + return curl_cmd + + def _get_caller_function_name(self) -> str: + stack = inspect.stack() + # for item in stack: + # print(inspect.getframeinfo(item[0]).function) + caller = stack[5] + frame = caller[0] + info = inspect.getframeinfo(frame) + return info.function + + def _print_debug(self, url, headers, payload, method) -> None: + print('-' * 80) + print(f'Caller function: {self._get_caller_function_name()}') + print('-' * 80) + print(f'Method: {method}\n') + print(f'URL:\n{url}\n') + print(f'Headers:\n{headers}\n') + print(f'Payload:\n{payload}\n') + print(f'cURL command:\n{self._curl_cmd(url, method, headers, payload)}') + print('-' * 80) @retry( wait=wait_fixed(30), @@ -58,6 +104,9 @@ async def get_data_async( url = f'{url}?{parameters.lstrip("&")}' method = 'POST' if payload else 'GET' + if self.debug: + self._print_debug(url, headers, payload, method) + async with httpx.AsyncClient() as client: r = await client.request( method, url, timeout=self._timeout, headers=headers, json=payload) diff --git a/semanticscholar/AsyncSemanticScholar.py b/semanticscholar/AsyncSemanticScholar.py index edb1b4b..e0569fa 100644 --- a/semanticscholar/AsyncSemanticScholar.py +++ b/semanticscholar/AsyncSemanticScholar.py @@ -25,13 +25,15 @@ def __init__( self, timeout: int = 10, api_key: str = None, - api_url: str = None + api_url: str = None, + debug: bool = False ) -> None: ''' :param float timeout: (optional) an exception is raised\ if the server has not issued a response for timeout seconds. :param str api_key: (optional) private API key. :param str api_url: (optional) custom API url. + :param bool debug: (optional) enable debug mode. ''' if api_url: @@ -43,7 +45,8 @@ def __init__( self.auth_header = {'x-api-key': api_key} self._timeout = timeout - self._requester = ApiRequester(self._timeout) + self._debug = debug + self._requester = ApiRequester(self._timeout, self._debug) @property def timeout(self) -> int: @@ -59,6 +62,21 @@ def timeout(self, timeout: int) -> None: ''' self._timeout = timeout self._requester.timeout = timeout + + @property + def debug(self) -> bool: + ''' + :type: :class:`bool` + ''' + return self._debug + + @debug.setter + def debug(self, debug: bool) -> None: + ''' + :param bool debug: + ''' + self._debug = debug + self._requester.debug = debug async def get_paper( self, diff --git a/semanticscholar/SemanticScholar.py b/semanticscholar/SemanticScholar.py index d983283..2bfa754 100644 --- a/semanticscholar/SemanticScholar.py +++ b/semanticscholar/SemanticScholar.py @@ -17,20 +17,24 @@ def __init__( self, timeout: int = 10, api_key: str = None, - api_url: str = None + api_url: str = None, + debug: bool = False ) -> None: ''' :param float timeout: (optional) an exception is raised\ if the server has not issued a response for timeout seconds. :param str api_key: (optional) private API key. :param str api_url: (optional) custom API url. + :param bool debug: (optional) enable debug mode. ''' nest_asyncio.apply() self._timeout = timeout + self._debug = debug self._AsyncSemanticScholar = AsyncSemanticScholar( timeout=timeout, api_key=api_key, - api_url=api_url + api_url=api_url, + debug=debug ) @property @@ -47,6 +51,21 @@ def timeout(self, timeout: int) -> None: ''' self._timeout = timeout self._AsyncSemanticScholar.timeout = timeout + + @property + def debug(self) -> bool: + ''' + :type: :class:`bool` + ''' + return self._debug + + @debug.setter + def debug(self, debug: bool) -> None: + ''' + :param bool debug: + ''' + self._debug = debug + self._AsyncSemanticScholar.debug = debug def get_paper( self, diff --git a/tests/data/debug_output.txt b/tests/data/debug_output.txt new file mode 100644 index 0000000..b39632d --- /dev/null +++ b/tests/data/debug_output.txt @@ -0,0 +1,17 @@ +-------------------------------------------------------------------------------- +Caller function: get_papers +-------------------------------------------------------------------------------- +Method: POST + +URL: +https://api.semanticscholar.org/graph/v1/paper/batch?fields=abstract,authors,citationCount,corpusId,externalIds,fieldsOfStudy,influentialCitationCount,isOpenAccess,journal,openAccessPdf,paperId,publicationDate,publicationTypes,publicationVenue,referenceCount,s2FieldsOfStudy,title,url,venue,year + +Headers: +{'x-api-key': 'F@k3K3y'} + +Payload: +{'ids': ['CorpusId:470667', '10.2139/ssrn.2250500', '0f40b1f08821e22e859c6050916cec3667778613']} + +cURL command: +curl -X POST -H 'x-api-key: F@k3K3y' -d '{"ids": ["CorpusId:470667", "10.2139/ssrn.2250500", "0f40b1f08821e22e859c6050916cec3667778613"]}' https://api.semanticscholar.org/graph/v1/paper/batch?fields=abstract,authors,citationCount,corpusId,externalIds,fieldsOfStudy,influentialCitationCount,isOpenAccess,journal,openAccessPdf,paperId,publicationDate,publicationTypes,publicationVenue,referenceCount,s2FieldsOfStudy,title,url,venue,year +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/tests/data/test_debug.yaml b/tests/data/test_debug.yaml new file mode 100644 index 0000000..6ba5993 --- /dev/null +++ b/tests/data/test_debug.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: '{"ids": ["CorpusId:470667", "10.2139/ssrn.2250500", "0f40b1f08821e22e859c6050916cec3667778613"]}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '96' + content-type: + - application/json + host: + - api.semanticscholar.org + user-agent: + - python-httpx/0.25.1 + x-api-key: + - F@k3K3y + method: POST + uri: https://api.semanticscholar.org/graph/v1/paper/batch?fields=abstract,authors,citationCount,corpusId,externalIds,fieldsOfStudy,influentialCitationCount,isOpenAccess,journal,openAccessPdf,paperId,publicationDate,publicationTypes,publicationVenue,referenceCount,s2FieldsOfStudy,title,url,venue,year + response: + content: '{"message":"Forbidden"}' + headers: + Connection: + - keep-alive + Content-Length: + - '23' + Content-Type: + - application/json + Date: + - Fri, 01 Dec 2023 17:42:22 GMT + Via: + - 1.1 0e3b340d2469d5d03795669e20d22b7e.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - 5GOaOSmWlLCOLN3BYU8v4ZYeCVwcLY3rdViECkesvkiObdK3RVAAKw== + X-Amz-Cf-Pop: + - GRU3-P4 + X-Cache: + - Error from cloudfront + x-amz-apigw-id: + - PRkn3G9vvHcETAQ= + x-amzn-ErrorType: + - ForbiddenException + x-amzn-RequestId: + - b5de114b-ac94-4023-9e40-17bf4713124d + http_version: HTTP/1.1 + status_code: 403 +version: 1 diff --git a/tests/test_semanticscholar.py b/tests/test_semanticscholar.py index 80a31e8..076e003 100644 --- a/tests/test_semanticscholar.py +++ b/tests/test_semanticscholar.py @@ -1,4 +1,6 @@ +import io import json +import sys import unittest import asyncio from datetime import datetime @@ -411,6 +413,24 @@ def test_empty_paginated_results(self): data = self.sch.search_paper('n0 r3sult s3arch t3rm') self.assertEqual(data.total, 0) + @test_vcr.use_cassette + def test_debug(self): + with open('tests/data/debug_output.txt', 'r') as file: + expected_output = file.read() + captured_stdout = io.StringIO() + sys.stdout = captured_stdout + self.sch = SemanticScholar(debug=True, api_key='F@k3K3y') + self.assertEqual(self.sch.debug, True) + list_of_paper_ids = [ + 'CorpusId:470667', + '10.2139/ssrn.2250500', + '0f40b1f08821e22e859c6050916cec3667778613'] + with self.assertRaises(PermissionError): + self.sch.get_papers(list_of_paper_ids) + sys.stdout = sys.__stdout__ + self.assertEqual(captured_stdout.getvalue().strip(), + expected_output.strip()) + class AsyncSemanticScholarTest(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: