diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f7e9d31 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2010-2023 Grepper, Inc. (https://www.grepper.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 5bcea0b..9de5852 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,148 @@ -# grepper-python -python client library +# Grepper Python Client +The Grepper Python library provides convenient access to the Grepper API from applications written in the Python language. + +## Requirements +Python 3.7 and later. + +## PIP +``` +pip install grepper-python +``` + +## Manual Installation +```bash +git clone https://github.com/CantCode023/grepper-python +cd grepper-python +python setup.py install +``` + + +# Usage + +## Initialising a new Grepper Class +The following code initialises a new `Grepper` Class. + +```py +from grepper_python import Grepper + +grepper = Grepper("Your grepper API Key") +``` +> Visit [Grepper Account Settings](https://www.grepper.com/app/settings-account.php) to get your API key. + + +## GrepperAnswer Type Class Reference +```py +class GrepperAnswer: + id: int + content: str + author_name: str + author_profile_url: str + title: str + upvotes: int + downvotes: int +``` +Refer the above type class whenever you see `GrepperAnswer` + +--- +## 1. Search function + +This function searches all answers based on a query. + +### Required parameters + +1. ``query (str, optional)``: Query to search through answer titles. +2. ``similarity (Optional[int], optional)``: How similar the query has to be to the answer title. 1-100 where 1 is really loose matching and 100 is really strict/tight match. Defaults to 60. + +### Returned value + +list[[GrepperAnswer](#grepperanswer-type-class-reference)] + +### Example: + +```py +from grepper_python import Grepper + +grepper = Grepper("YOUR_API_KEY") +data = grepper.search("git abort command") + +# Returns list of GrepperAnswer objects +for answer in data: + print(answer.id) + print(answer.content) + print(answer.author_name) + print(answer.author_profile_url) + print(answer.title) + print(answer.upvotes) + print(answer.downvotes) +``` + +### Output + +``` +674394 +git merge --abort +git rebase --abort +# if you are still into some command on git and got back # +Magnificent Moose +https://www.grepper.com/profile/mou-biswas +abort a git command +0 +0 +``` +--- + +## 2. fetch_answer +This function returns an answer specified by the id. + +### Required parameters + - `id (int, required)`: The id for the specified answer. ex: 504956. +### Result +fetch_answer returns `GrepperAnswer` type class on successful search. + +### Examples +```py +grepper = Grepper("YOUR_API_KEY") +answer = grepper.fetch_answer(504956) +print(answer) +``` +### Output +```py +{ + "id": 504956, + "content": "var arr=[1,2,3];\narr.reverse().forEach(x=> console.log(x))", + "author_name": "Yanislav Ivanov", + "author_profile_url": "https://www.grepper.com/profile/yanislav-ivanov-r2lfrl14s6xy", + "title": "js loop array back", + "upvotes": 2, + "object": "answer", + "downvotes": 2 +} +``` +--- + +## 3. update_answer +This function updates/edits the answer of specified id. +- `NOTE:` This endpoint is in progress and not yet available according to grepper API docs. + +### Required parameters + - `id (int, required)`: The id for the specified answer. ex: "504956 ". + - `answer (str, required)`: The answer you want it to update to. ex "new answer content here". + +### Result +returns a `Dict` + +### Example +```py +grepper = Grepper("YOUR_API_KEY") +result = grepper.update_answer(id=504956,answer="The new edited answer") +print(result) +``` + +### Output +```py +{ + id: 2, + success: "true" +} +``` +--- diff --git a/grepper_python/__init__.py b/grepper_python/__init__.py new file mode 100644 index 0000000..e1edaf1 --- /dev/null +++ b/grepper_python/__init__.py @@ -0,0 +1,26 @@ +""" +Python Grepper API +~~~~~~~~~~~~~~~~~~~ +An API wrapper for the Grepper API. +""" + +__title__ = "grepper-python" +__author__ = "CodeGrepper" +__license__ = "MIT" +__copyright__ = "Copyright 2010-2023 Grepper, Inc." +__version__ = "0.0.1a" + +from grepper_python.answer import GrepperAnswer +from grepper_python.main import Grepper +from grepper_python.exceptions import ( + BadRequest, + Unauthorized, + Forbidden, + NotFound, + MethodNotAllowed, + TooManyRequests, + InternalServerError, + ServiceUnavailable +) + +__all__ = ["GrepperAnswer", "Grepper", "BadRequest", "Unauthorized", "Forbidden", "NotFound", "MethodNotAllowed", "TooManyRequests", "InternalServerError", "ServiceUnavailable"] \ No newline at end of file diff --git a/grepper_python/answer.py b/grepper_python/answer.py new file mode 100644 index 0000000..9ecf806 --- /dev/null +++ b/grepper_python/answer.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + + +@dataclass +class GrepperAnswer: + id: int + content: str + author_name: str + author_profile_url: str + title: str + upvotes: int + downvotes: int diff --git a/grepper_python/exceptions.py b/grepper_python/exceptions.py new file mode 100644 index 0000000..8217d47 --- /dev/null +++ b/grepper_python/exceptions.py @@ -0,0 +1,48 @@ +""" +grepper-python.exceptions +~~~~~~~~~~~~~~~~~~~ +This module contains the set of grepper-python's exceptions. +""" + +class GrepperDefaultException(Exception): + def __init__(self, msg=None, *args, **kwargs): + super().__init__(msg or self.__doc__, *args, **kwargs) + +# 400 +class BadRequest(GrepperDefaultException): + """Your request is invalid.""" + + +# 401 +class Unauthorized(GrepperDefaultException): + """Your API key is wrong.""" + + +# 403 +class Forbidden(GrepperDefaultException): + """You do not have access to the requested resource.""" + + +# 404 +class NotFound(GrepperDefaultException): + """The specified enpoint could not be found.""" + + +# 405 +class MethodNotAllowed(GrepperDefaultException): + """You tried to access an enpoint with an invalid method.""" + + +# 429 +class TooManyRequests(GrepperDefaultException): + """You're making too many requests! Slow down!""" + + +# 500 +class InternalServerError(GrepperDefaultException): + """We had a problem with our server. Try again later.""" + + +# 503 +class ServiceUnavailable(GrepperDefaultException): + """We're temporarily offline for maintenance. Please try again later.""" diff --git a/grepper_python/main.py b/grepper_python/main.py new file mode 100644 index 0000000..931dd3d --- /dev/null +++ b/grepper_python/main.py @@ -0,0 +1,158 @@ +""" +The MIT License + +Copyright (c) 2010-2023 Grepper, Inc. (https://www.grepper.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import Optional, List + +from grepper_python.answer import GrepperAnswer +from grepper_python.exceptions import ( + BadRequest, + Unauthorized, + Forbidden, + NotFound, + MethodNotAllowed, + TooManyRequests, + InternalServerError, + ServiceUnavailable +) + +import requests + + +def exception_handler(status_code): + if status_code == 400: + return BadRequest + elif status_code == 401: + return Unauthorized + elif status_code == 403: + return Forbidden + elif status_code == 404: + return NotFound + elif status_code == 405: + return MethodNotAllowed + elif status_code == 429: + return TooManyRequests + elif status_code == 500: + return InternalServerError + elif status_code == 503: + return ServiceUnavailable + + +class Grepper: + """ + Python Grepper API Wrapper + """ + def __init__(self, api_key: str): + self._api_key = api_key + + def search( + self, query: str = False, similarity: Optional[int] = 60 + ) -> List[GrepperAnswer]: + """This function searches all answers based on a query. + + Args: + query (str, optional): Query to search through answer titles. ex: "Javascript loop array backwords". Defaults to False. + similarity (Optional[int], optional): How similar the query has to be to the answer title. 1-100 where 1 is really loose matching and 100 is really strict/tight match. Defaults to 60. + + Returns: + GrepperAnswer + """ + response = requests.get( + "https://api.grepper.com/v1/answers/search", + params={"query": query, "similarity": similarity}, + auth=(self._api_key, ""), + ) + if response.status_code != 200: + raise exception_handler(response.status_code) + json_response = response.json() + data = [] + for answer in json_response["data"]: + new_answer = GrepperAnswer( + id=answer["id"], + content=answer["content"], + author_name=answer["author_name"], + author_profile_url=answer["author_profile_url"], + title=answer["title"], + upvotes=answer["upvotes"], + downvotes=answer["downvotes"], + ) + data.append(new_answer) + return data + + def fetch_answer( + self, id: int + ) -> GrepperAnswer: + """This function returns an answer specified by the id. + + Args: + id (int, required): The id for the specified answer. ex: "560676 ". + + Returns: + GrepperAnswer + """ + response = requests.get( + f"https://api.grepper.com/v1/answers/{id}", + auth=(self._api_key, "") + ) + if response.status_code != 200: + raise exception_handler(response.status_code) + json_response = response.json() + answer = GrepperAnswer( + id=json_response["id"], + content=json_response["content"], + author_name=json_response["author_name"], + author_profile_url=json_response["author_profile_url"], + title=json_response["title"], + upvotes=json_response["upvotes"], + downvotes=json_response["downvotes"], + ) + return answer + + def update_answer( + self, id: int, answer: str + ): + """This function returns an answer specified by the id. + + Args: + id (int, required): The id for the specified answer. ex: "560676 ". + answer (str, required): The answer you want it to update to. ex "new answer content here". + + Returns: + Dict + """ + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + data = f"""answer[content]={answer}""" + response = requests.post( + f"https://api.grepper.com/v1/answers/{id}", + headers=headers, + data=data, + auth=(self._api_key, "") + ) + if response.status_code != 200: + raise exception_handler(response.status_code) + else: + return response.json() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3c73adb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "grepper-python" +dynamic = ["version"] +dependencies = [ + "requests", + "urllib3" +] +authors = [ + {name = "CodeGrepper", email = "support@grepper.com"}, +] +description = "An API wrapper for the Grepper API." +readme = "README.md" +license = {file = "LICENSE"} +classifiers = [ + "Programming Language :: Python :: 3", +] +requires-python = ">=3.7" + +[project.urls] +Repository="https://github.com/CodeGrepper/grepper-python" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f7cdd11 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup_info = { + "name": "grepper-python", + "version": "0.0.1a", + "author": "CodedGrepper", + "author_email": "support@grepper.com", + "description": "An API wrapper for the Grepper API.", + "long_description": long_description, + "long_description_content_type": "text/markdown", + "url": "https://github.com/CodeGrepper/grepper-python", + "packages": setuptools.find_packages(), + "install_requires": ["requests", "urllib3"], + "classifiers": [ + "Programming Language :: Python :: 3", + ], + "python_requires": '>=3.7' +} + + +setuptools.setup(**setup_info) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_fetch_answer.py b/tests/test_fetch_answer.py new file mode 100644 index 0000000..c3fcfa7 --- /dev/null +++ b/tests/test_fetch_answer.py @@ -0,0 +1,65 @@ +import unittest +from unittest.mock import patch + +from grepper_python import Grepper +from grepper_python.exceptions import NotFound + + +class Test(unittest.TestCase): + def setUp(self): + self.api_key = "my_api_key" + self.grepper = Grepper(self.api_key) + + @patch("requests.get") + def test_fetch_answer_success(self, mock_get): + mock_response = mock_get.return_value + mock_response.status_code = 200 + mock_response.json.return_value = { + "id": 12345, + "title": "Sample answer title", + "content": "Sample answer content", + "author_name": "John Doe", + "author_profile_url": "https://www.example.com/johndoe", + "upvotes": 10, + "downvotes": 0, + } + + answer = self.grepper.fetch_answer(12345) + + # Assertions + self.assertEqual(answer.id, 12345) + self.assertEqual(answer.title, "Sample answer title") + self.assertEqual(answer.content, "Sample answer content") + self.assertEqual(answer.author_name, "John Doe") + self.assertEqual(answer.author_profile_url, "https://www.example.com/johndoe") + self.assertEqual(answer.upvotes, 10) + self.assertEqual(answer.downvotes, 0) + + @patch("requests.get") + def test_fetch_answer_not_found(self, mock_get): + mock_response = mock_get.return_value + mock_response.status_code = 404 + mock_response.text = "Not Found" + + with self.assertRaises(NotFound) as cm: + self.grepper.fetch_answer(12345) + + self.assertEqual(str(cm.exception), "HTTPException: Not Found") + + @patch("requests.get") + def test_fetch_answer_other_error(self, mock_get): + mock_response = mock_get.return_value + mock_response.status_code = 500 + mock_response.text = "Internal Server Error" + + with self.assertRaises(Exception) as cm: + self.grepper.fetch_answer(12345) + + self.assertEqual( + str(cm.exception), + "HTTPException: Unexpected status code: 500 (Internal Server Error)", + ) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..cf94648 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,63 @@ +import unittest +from unittest.mock import patch, Mock +from grepper_python import Grepper + + +class Test(unittest.TestCase): + def setUp(self): + self.api_key = "my_api_key" + self.grepper = Grepper(self.api_key) + + @patch("grepper_python.main.requests.get") + def test_search(self, mock_get): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "data": [ + { + "id": 1, + "content": "example content", + "author_name": "example author", + "author_profile_url": "https://example.com", + "title": "example title", + "upvotes": 10, + "downvotes": 2, + } + ] + } + mock_get.return_value = mock_response + + results = self.grepper.search( + query="example query", similarity=80 + ) + self.assertEqual(len(results), 1) + self.assertEqual(results[0].id, 1) + self.assertEqual(results[0].content, "example content") + self.assertEqual(results[0].author_name, "example author") + self.assertEqual( + results[0].author_profile_url, "https://example.com" + ) + self.assertEqual(results[0].title, "example title") + self.assertEqual(results[0].upvotes, 10) + self.assertEqual(results[0].downvotes, 2) + + mock_get.assert_called_once_with( + "https://api.grepper.com/v1/answers/search", + params={"query": "example query", "similarity": 80}, + auth=(self.api_key, ""), + ) + + @patch("grepper_python.main.exception_handler") + @patch("grepper_python.main.requests.get") + def test_search_with_error(self, mock_get, mock_exception_handler): + mock_response = Mock() + mock_response.status_code = 404 + mock_get.return_value = mock_response + + with self.assertRaises(Exception): + self.grepper.search(query="example query", similarity=80) + + mock_exception_handler.assert_called_once_with("404") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_update_answer.py b/tests/test_update_answer.py new file mode 100644 index 0000000..2e3b623 --- /dev/null +++ b/tests/test_update_answer.py @@ -0,0 +1,42 @@ +import unittest +from unittest.mock import patch + +from grepper_python import Grepper + + +class TestGrepper(unittest.TestCase): + def setUp(self): + self.api_key = "my_api_key" + self.grepper = Grepper(self.api_key) + + @patch("requests.post") + def test_update_answer_success(self, mock_post): + mock_response = mock_post.return_value + mock_response.status_code = 200 + mock_response.json.return_value = {"message": "Answer updated successfully"} + + answer_id = 12345 + new_content = "updated content" + + self.grepper.update_answer(answer_id, new_content) + mock_post.assert_called_once_with( + f"https://api.grepper.com/v1/answers/{answer_id}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data=f"answer[content]={new_content}", + auth=(self.api_key, ""), + ) + + @patch("requests.post") + def test_update_answer_error(self, mock_post): + mock_response = mock_post.return_value + mock_response.status_code = 401 + mock_response.text = "Unauthorized" + + with self.assertRaises(Exception) as cm: + self.grepper.update_answer(12345, "new answer content") + + self.assertEqual(str(cm.exception).startswith("HTTPException:"), True) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file