From 38dc56a68d6367d9f637eb2a32b216b1fe2c4b10 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Fri, 16 Oct 2020 22:12:43 +0530 Subject: [PATCH] New API update (#14) * feat: improve interface with requests * refactor: more logging statements now checks for status_code before parsing to json major refactor * try and make test pass * docs: update changelog * lint: isort * Update reference.rst * lint: remove extra comment * doc: fix typo * fix formatting --- docs/source/changelog.rst | 15 +++++- docs/source/reference.rst | 1 + sxcu/__client__.py | 73 ++++++++++++++++++++++++++ sxcu/__init__.py | 9 +++- sxcu/__logger__.py | 10 ++++ sxcu/__version__.py | 7 +++ sxcu/constants.py | 80 ++++++++++++++++++++++++++++ sxcu/sxcu.py | 106 ++++++++++++++++++++++++++++++-------- tests/test_api.py | 1 + 9 files changed, 278 insertions(+), 24 deletions(-) create mode 100644 sxcu/__client__.py create mode 100644 sxcu/__logger__.py create mode 100644 sxcu/__version__.py create mode 100644 sxcu/constants.py diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 53a98ad..acf0a8c 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,7 +8,20 @@ Changelog sxcu-v1.0.2 =========== -[TOB] + +* Introduced a logger class so that users can know what + really happened. User's can just set a logging handle and + it will automatically write logs as in any other library. + +* Improved Interface with Requests. Instead of Directly + calling it now logs them and goes through ``__client__``. + +* Moved version to ``__version__`` and added other meta data + to it. + +* Now it handle's server response codes. Previously it was + rising JSON decode Error which was unexpected and could cause + problems. sxcu-v1.0.1 diff --git a/docs/source/reference.rst b/docs/source/reference.rst index fc0e46c..cf7a46f 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -14,3 +14,4 @@ Python API Reference ~SXCU ~og_properties + ~__client__.RequestClient diff --git a/sxcu/__client__.py b/sxcu/__client__.py new file mode 100644 index 0000000..237b731 --- /dev/null +++ b/sxcu/__client__.py @@ -0,0 +1,73 @@ +import requests + +from .__logger__ import logger +from .constants import HEADERS + +__all__ = ["RequestClient"] + + +class RequestClient: + """``RequestClient`` is internally used to communicated with + ``Requests`` Library. + """ + + def __init__(self, headers: dict = None) -> None: + """This initaite the handlers. + Parameters + ========== + headers : :class:`str`, optional + The extra header needed to be added to the Request. + """ + if headers and isinstance(headers, dict): + self.headers = headers + else: + self.headers = HEADERS + logger.debug(f"Headers recieved are: {self.headers}") + + def post( + self, url: str, headers: dict = None, **kwargs # noqa ANN003 + ) -> requests.models.Response: + """Pass all the parameter to :func:`requests.post`. + Also, adding the neccessary headers. Also, the newly passed header + would overide the default. + Parameters + ========== + headers : :class:`str`, optional + The header needed to be added to the Request. + + .. important :: + + The header would overide the default header. + """ + logger.debug(f"Trying to do a Post Requests to {url}") + headers = self.headers if headers is None else headers + con = requests.post(url, headers=headers, **kwargs) + logger.debug(f"Recieved Headers from {url}: {con.headers}") + logger.debug(f"status_code returned was:{con.status_code}") + response = con.text + logger.info(f"Recieved Response: {response}") + return con + + def get( + self, url: str, headers: dict = None, **kwargs # noqa ANN003 + ) -> requests.models.Response: + """Pass all the parameter to :func:`requests.get`. + Also, adding the neccessary headers. Also, the newly passed header + would overide the default. + Parameters + ========== + headers : :class:`str`, optional + The header needed to be added to the Request. + + .. important :: + + The header would overide the default header. + """ + logger.debug(f"Trying to do Get Requests to {url}") + headers = self.headers if headers is None else headers + con = requests.get(url=url, headers=headers, **kwargs) + logger.debug(f"Recieved Headers from {url}: {con.headers}") + # TODO: Don't use json instead implement a custom class here. + response = con.text + logger.info(f"Recieved Response: {response}") + return con diff --git a/sxcu/__init__.py b/sxcu/__init__.py index 1a5c5d7..a09b0c5 100644 --- a/sxcu/__init__.py +++ b/sxcu/__init__.py @@ -8,6 +8,11 @@ :license: Apache-2.0 , see LICENSE for details. """ +from .__version__ import __author__ # noqa F401 +from .__version__ import __copyright__ # noqa F401 +from .__version__ import __description__ # noqa F401 +from .__version__ import __license__ # noqa F401 +from .__version__ import __title__ # noqa F401 +from .__version__ import __url__ # noqa F401 +from .__version__ import __version__ # noqa F401 from .sxcu import SXCU, og_properties # noqa F401 - -__version__ = "1.0.1" diff --git a/sxcu/__logger__.py b/sxcu/__logger__.py new file mode 100644 index 0000000..4c9bb54 --- /dev/null +++ b/sxcu/__logger__.py @@ -0,0 +1,10 @@ +import logging + +logger = logging.getLogger("sxcu") +s_handler = logging.StreamHandler() +s_handler.setLevel(logging.DEBUG) +s_format = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +s_handler.setFormatter(s_format) +logger.addHandler(s_handler) diff --git a/sxcu/__version__.py b/sxcu/__version__.py new file mode 100644 index 0000000..7926e00 --- /dev/null +++ b/sxcu/__version__.py @@ -0,0 +1,7 @@ +__title__ = "sxcu" +__description__ = "Python API wraper for sxcu.net" +__url__ = "https://sxcu.syrusdark.website" +__version__ = "1.0.1" +__author__ = "Naveen M K" +__license__ = "Apache 2.0" +__copyright__ = "Copyright 2020 Naveen M K" diff --git a/sxcu/constants.py b/sxcu/constants.py new file mode 100644 index 0000000..5c4ab91 --- /dev/null +++ b/sxcu/constants.py @@ -0,0 +1,80 @@ +from .__version__ import __version__ + +status_code_upload_image = { + "403": { + "message": "Invalid upload token", + "desc": "The specified upload token does not match the domain's upload token.", + }, + "404": { + "message": "Collection not found", + "desc": "The specified collection was not found.", + }, + "405": { + "message": "Request is not POST", + "desc": "The request method must be POST.", + }, + "406": { + "message": "Upload error 101x", + "desc": "An error occurred while handling the uploaded file.", + }, + "407": { + "message": "Subdomain is private, a valid upload token is required", + "desc": "The sub domain you tried to upload to is private, and requires a valid upload token in order to upload to it.", + }, + "409": { + "message": "No file sent", + "desc": "No binary file was sent in the 'image' field.", + }, + "410": { + "message": "Collection is private but no collection token provided", + "desc": "The collection you tried to upload to is set to private and requires a collection token in order to upload to it.", + }, + "412": { + "message": "User-agent header not set", + "desc": "The request did not contain a User-Agent header.", + }, + "413": { + "message": "File is over the size limit", + "desc": "Uploaded file is larger than 95 MB.", + }, + "415": { + "message": "File type not allowed.", + "desc": "The type of the uploaded file is not supported.", + }, + "416": { + "message": "Invalid collection token", + "desc": "The specified collection token does not match the collection's token.", + }, + "422": { + "message": "Malformed JSON in OpenGraph properties", + "desc": "The OpenGraph properties JSON array could not be properly parsed, and is most likely malformed.", + }, + "429": {"message": None, "desc": "The request exceeded the rate limit."}, + "500": { + "message": "The file was not uploaded due to an unknown error", + "desc": "An unknown error has occurred while processing the file, try again later.", + }, +} +status_code_upload_text = { + "409": { + "message": None, + "desc": "The text POST param is missing.", + }, + "413": { + "message": None, + "desc": "Text is too long (8 MB).", + }, + "429": {"message": None, "desc": "The request exceeded the rate limit."}, +} +status_code_create_link = { + "400": { + "message": None, + "desc": "The text POST param is missing or invalid URL", + }, + "429": {"message": None, "desc": "The request exceeded the rate limit."}, +} + +status_code_general = { + "429": {"message": None, "desc": "The request exceeded the rate limit."}, +} +HEADERS = {"User-Agent": f"python-sxcu-{__version__}"} diff --git a/sxcu/sxcu.py b/sxcu/sxcu.py index aadb20a..30546be 100644 --- a/sxcu/sxcu.py +++ b/sxcu/sxcu.py @@ -1,12 +1,21 @@ -"""Python API wrapper for sxcu.net subdomains +"""Python API wrapper for sxcu.net """ import json from typing import Union -import requests +from .__client__ import RequestClient +from .__logger__ import logger +from .constants import ( + status_code_create_link, + status_code_general, + status_code_upload_image, + status_code_upload_text, +) __all__ = ["og_properties", "SXCU"] +request_handler = RequestClient() + class og_properties: """ @@ -60,7 +69,7 @@ def __init__( The content in ``.scxu`` file has more priority than passed parameters. """ self.subdomain = subdomain if subdomain else "https://sxcu.net" - self.upload_token = upload_token + self.upload_token = upload_token # Not logging upload_token self.file_sxcu = file_sxcu self.api_endpoint = "https://sxcu.net/api" if file_sxcu: @@ -70,6 +79,7 @@ def __init__( self.subdomain = "/".join(con["RequestURL"].split("/")[:-1]) if "Arguments" in con: self.upload_token = con["Arguments"]["token"] + logger.debug(f"subdomain is:{self.subdomain}") def upload_image( self, @@ -119,8 +129,16 @@ def upload_image( ) with open(file, "rb") as img_file: files = {"image": img_file} - res = requests.post(url=url, files=files, data=data) - return res.json() + res = request_handler.post(url, files=files, data=data) + if res.status_code in status_code_upload_image: + logger.error( + f"The status_code was {res.status_code} which was expected to be 200." + ) + logger.error( + f"The reason for this error is {status_code_upload_image['desc']}" + ) + raise Exception(status_code_upload_image["desc"]) + return res.json() # TODO: Don't use json instead implement a custom class here. def create_link(self, link: str) -> Union[dict, list]: """Creates a new link. @@ -140,8 +158,16 @@ def create_link(self, link: str) -> Union[dict, list]: if self.subdomain[-1] == "/" else self.subdomain + "/shorten" ) - con = requests.post(url, data={"link": link}) - return con.json() + res = request_handler.post(url, data={"link": link}) + if res.status_code in status_code_create_link: + logger.error( + f"The status_code was {res.status_code} which was expected to be 200." + ) + logger.error( + f"The reason for this error is {status_code_create_link['desc']}" + ) + raise Exception(status_code_create_link["desc"]) + return res.json() @staticmethod def edit_collection( @@ -199,8 +225,14 @@ def edit_collection( data["empty_collection"] = "" if delete_collection: data["delete_collection"] = "" - con = requests.post("https://sxcu.net/api/", data=data) - final = con.json() + res = request_handler.post("https://sxcu.net/api/", data=data) + if res.status_code in status_code_general: + logger.error( + f"The status_code was {res.status_code} which was expected to be 200." + ) + logger.error(f"The reason for this error is {status_code_general['desc']}") + raise Exception(status_code_general["desc"]) + final = res.json() if isinstance(final, list): final = dict() final["token"] = None @@ -242,8 +274,14 @@ def create_collection( } if desc: data["desc"] = desc - con = requests.post("https://sxcu.net/api/", data=data) - return con.json() + res = request_handler.post("https://sxcu.net/api/", data=data) + if res.status_code in status_code_general: + logger.error( + f"The status_code was {res.status_code} which was expected to be 200." + ) + logger.error(f"The reason for this error is {status_code_general['desc']}") + raise Exception(status_code_general["desc"]) + return res.json() @staticmethod def collection_details(collection_id: str) -> Union[dict, list]: @@ -259,8 +297,14 @@ def collection_details(collection_id: str) -> Union[dict, list]: :class:`dict` or :class:`list` The returned JSON from the request. """ - con = requests.get(f"https://sxcu.net/c/{collection_id}.json") - return con.json() + res = request_handler.get(f"https://sxcu.net/c/{collection_id}.json") + if res.status_code in status_code_general: + logger.error( + f"The status_code was {res.status_code} which was expected to be 200." + ) + logger.error(f"The reason for this error is {status_code_general['desc']}") + raise Exception(status_code_general["desc"]) + return res.json() @staticmethod def upload_text(text: str) -> Union[dict, list]: @@ -276,8 +320,16 @@ def upload_text(text: str) -> Union[dict, list]: :class:`dict` or :class:`list` The returned JSON from the request. """ - con = requests.post("https://cancer-co.de/upload", data={"text": text}) - return con.json() + res = request_handler.post("https://cancer-co.de/upload", data={"text": text}) + if res.status_code in status_code_upload_text: + logger.error( + f"The status_code was {res.status_code} which was expected to be 200." + ) + logger.error( + f"The reason for this error is {status_code_upload_image['desc']}" + ) + raise Exception(status_code_upload_text["desc"]) + return res.json() @staticmethod def image_details(image_id: str = None, image_url: str = None) -> Union[dict, list]: @@ -307,8 +359,14 @@ def image_details(image_id: str = None, image_url: str = None) -> Union[dict, li image_url = f"https://sxcu.net/{image_id}.json" if image_url[-5:-1] != ".json": image_url += ".json" - con = requests.get(image_url) - return con.json() + res = request_handler.get(image_url) + if res.status_code in status_code_general: + logger.error( + f"The status_code was {res.status_code} which was expected to be 200." + ) + logger.error(f"The reason for this error is {status_code_general['desc']}") + raise Exception(status_code_general["desc"]) + return res.json() @staticmethod def domain_list(count: int = -1) -> list: @@ -329,11 +387,17 @@ def domain_list(count: int = -1) -> list: :class:`list` The returned JSON from the request. """ - con = requests.get("https://sxcu.net/api?action=domains") + res = request_handler.get("https://sxcu.net/api?action=domains") + if res.status_code in status_code_general: + logger.error( + f"The status_code was {res.status_code} which was expected to be 200." + ) + logger.error(f"The reason for this error is {status_code_general['desc']}") + raise Exception(status_code_general["desc"]) if count == -1: - to_encode = con.json() + to_encode = res.json() else: - to_encode = con.json()[:count] + to_encode = res.json()[:count] for i in enumerate(to_encode): temp = {} for j in i[1]: @@ -355,5 +419,5 @@ def delete_image(delete_url: str) -> bool: :class:`bool` Deleted or not """ - con = requests.get(delete_url) + con = request_handler.get(delete_url) return bool(con.status_code == 200) diff --git a/tests/test_api.py b/tests/test_api.py index e8dd083..5480dae 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -29,6 +29,7 @@ def test_ogproperties() -> None: def test_upload_keys_default_domain() -> None: + time.sleep(60) from sxcu import SXCU t = SXCU()