diff --git a/python/compress-image/README.md b/python/compress-image/README.md new file mode 100644 index 00000000..24267bd0 --- /dev/null +++ b/python/compress-image/README.md @@ -0,0 +1,81 @@ +# đŸ–ŧī¸ Compress Image with TinyPNG and KrakenIO + +A Python Cloud Function for compressing images without losing quality using [Tinypng API](https://tinypng.com/) and [KrakenIO](https://kraken.io/). + + +Example input with Tinypng: +```json +{ + "provider":"tinypng", + "image":"iVBORw0KGgoAAAANSUhEUgAAAaQAAALiCAY...QoH9hbkTPQAAAABJRU5ErkJggg==" +} +``` +Example input with KrakenIO: +```json +{ + "provider":"krakenio", + "image":"iVBORw0KGgoAAAANSUhEUgAAAaQAAALiCAY...QoH9hbkTPQAAAABJRU5ErkJggg==" +} +``` + +Example output: +```json +{ + "success":true, + "image":"iVBORw0KGgoAAAANSUhE...o6Ie+UAAAAASU5CYII=" +} +``` +Example error output: +```json +{ + "success":false, + "image":"iVBORw0KGgoAAAANSUhE...o6Ie+UAAAAASU5CYII=" +} +``` + +## 📝 Environment Variables + +List of environment variables used by this cloud function: +- **TINYPNG_KEY** - Tinypng API Key +- **KRAKENIO_KEY** - KrakenIO API Key +- **KRAKENIO_SECRET_KEY** - KrakenIO Secret API Key + + +â„šī¸ _Create your TinyPNG API key at https://tinypng.com/developers_.
+â„šī¸ _Create your KrakenIO API key at https://kraken.io/docs/getting-started_.
+ + +## 🚀 Deployment + +1. Clone this repository, and enter this function folder: + +```bash +git clone https://github.com/open-runtimes/examples.git +cd examples/python/compress-image +``` + +2. Enter this function folder and build the code: +```bash +docker run --rm --interactive --tty --volume $PWD:/usr/code openruntimes/python:v2-3.10 sh /usr/local/src/build.sh +``` +As a result, a `code.tar.gz` file will be generated. + +3. Start the Open Runtime: +```bash +docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key -e INTERNAL_RUNTIME_ENTRYPOINT=main.py --rm --interactive --tty --volume $PWD/code.tar.gz:/tmp/code.tar.gz:ro openruntimes/python:v2-3.10 sh /usr/local/src/start.sh +``` + +> Make sure to replace `YOUR_API_KEY` with your key. +Your function is now listening on port `3000`, and you can execute it by sending `POST` request with appropriate authorization headers. To learn more about runtime, you can visit Python runtime [README](https://github.com/open-runtimes/open-runtimes/tree/main/openruntimes/python:v2-3.10). +4. Run the cURL function to send request. +>TinyPNG Curl Example (Supports only API_KEY in Environment Variables) +```bash +curl http://localhost:3000/ -H "X-Internal-Challenge: secret-key" -H "Content-Type: application/json" -d '{"payload": {"provider": "tinypng", "image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+L+U4T8ABu8CpCYJ1DQAAAAASUVORK5CYII="}, "variables": {"API_KEY": ""}}' +``` +>KrakenIO Curl Example (Supports API_KEY and SECRET_API_KEY in Environment Variables) +```bash +curl http://localhost:3000/ -H "X-Internal-Challenge: secret-key" -H "Content-Type: application/json" -d '{"payload": {"provider": "krakenio", "image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+L+U4T8ABu8CpCYJ1DQAAAAASUVORK5CYII="}, "variables": {"API_KEY": "", "SECRET_API_KEY": ""}}' +``` +## 📝 Notes +- This function is designed for use with Appwrite Cloud Functions. You can learn more about it in [Appwrite docs](https://appwrite.io/docs/functions). +- This example is compatible with Python 3.10. Other versions may work but are not guaranteed to work as they haven't been tested. diff --git a/python/compress-image/main.py b/python/compress-image/main.py new file mode 100644 index 00000000..d9b40555 --- /dev/null +++ b/python/compress-image/main.py @@ -0,0 +1,147 @@ +""" Compress image function using Tinypng and Krakenio API.""" +import base64 +import json +import tinify +import requests + +KRAKEN_API_ENDPOINT = "https://api.kraken.io/v1/upload" +KRAKEN_USER_AGENT = ( + "Mozilla/5.0 (Windows NT 6.1; Win64; x64)AppleWebKit/" + "537.36(KHTML, like Gecko)Chrome/40.0.2214.85 Safari/537.36" +) + + +def implement_krakenio(variables): + """ + Implements image optimization using the Kraken.io API. + + Input: + variables (dict): A dictionary containing the + required variables for optimization. + Returns: + optimized_image (bytes): decoded optimized image. + Raises: + raise_for_status (method): raise an HTTPError if the HTTP request + returned an unsuccessful status code. + """ + # Headers for post request + headers = {"User-Agent": KRAKEN_USER_AGENT} + # Image that we will pass in + files = {"file": variables["decoded_image"]} + # Parameters for post request + params = { + "auth": { + "api_key": variables["api_key"], + "api_secret": variables["api_secret_key"] + }, + "wait": True, # Optional: Wait for the optimization to complete. + "dev": False, # Optional: Set to false to enter user mode. + } + response = requests.post( + url=KRAKEN_API_ENDPOINT, + headers=headers, + files=files, + data={"data": json.dumps(params)}, + timeout=10, + ) + # Check status code of response + response.raise_for_status() + data = response.json() + # Response unsuccessful, raise error + if not data["success"]: + raise ValueError("KrakenIO was not able to compress image.") + # Response successful, parse the response + optimized_url = data["kraked_url"] + optimized_image = requests.get(optimized_url, timeout=10).content + return optimized_image + + +def implement_tinypng(variables): + """ + Implements image optimization using the Tinypng API. + + Input: + variables (dict): A dictionary containing the required variables + for optimization. Includes api_key and decoded_image. + Returns: + tinify.from_buffer().tobuffer() (bytes): decoded optimized image. + Raises: + tinify.error (method): raised if tinify fails to compress image. + """ + tinify.key = variables["api_key"] + return tinify.from_buffer(variables["decoded_image"]).to_buffer() + + +def validate_request(req): + """ + Validates the request and extracts the necessary information. + + Input: + req (json): The request object containing the payload and variables. + Returns: + result (dict): Contains the validated request information. + Raises: + ValueError: If any required value is missing or invalid. + """ + # Check if payload is empty + if not req.payload: + raise ValueError("Missing payload.") + # Accessing provider from payload + if not req.payload.get("provider"): + raise ValueError("Missing provider.") + # Check if payload is not empty + if not req.variables: + raise ValueError("Missing variables.") + # Accessing api_key from variables + if not req.variables.get("API_KEY"): + raise ValueError("Missing API_KEY.") + # Accessing encoded image from payload + if not req.payload.get("image"): + raise ValueError("Missing encoding image.") + result = { + "provider": req.payload.get("provider").lower(), + "api_key": req.variables.get("API_KEY"), + "decoded_image": base64.b64decode(req.payload.get("image")), + } + # Get secret key + if req.payload.get("provider") == "krakenio": + if not req.variables.get("SECRET_API_KEY"): + raise ValueError("Missing api secret key.") + result["api_secret_key"] = req.variables.get("SECRET_API_KEY") + return result + + +def main(req, res): + """ + The main function that runs validate_request and calls IMPLEMENTATIONS. + + Input: + req (json): The request object. + res (json): The response object. + + Returns: + res (json): A JSON response containing the optimization results. + """ + try: + variables = validate_request(req) + except (ValueError) as value_error: + return res.json({ + "success": False, + "error": f"{value_error}", + }) + try: + if variables["provider"] == "tinypng": + optimized_image = implement_tinypng(variables) + elif variables["provider"] == "krakenio": + optimized_image = implement_krakenio(variables) + else: + raise ValueError("Invalid provider.") + except Exception as error: + return res.json({ + "success": False, + "error": f"{type(error).__name__}: {error}", + }) + return res.json({ + "success": True, + "image": base64.b64encode(optimized_image).decode(), + }) diff --git a/python/compress-image/requirements.txt b/python/compress-image/requirements.txt new file mode 100644 index 00000000..1fad875b --- /dev/null +++ b/python/compress-image/requirements.txt @@ -0,0 +1,3 @@ +tinify==1.6.0 +requests==2.31.0 +parameterized==0.9.0 \ No newline at end of file diff --git a/python/compress-image/secret.py b/python/compress-image/secret.py new file mode 100644 index 00000000..7952e6bb --- /dev/null +++ b/python/compress-image/secret.py @@ -0,0 +1,8 @@ +''' +API Key for tinyPNG and KrakenIO are stored here + +You should be in python/compress-image directory to run test_main.py +''' +API_KEY_TINYPNG = None +API_KEY_KRAKENIO = None +SECRET_API_KEY_KRAKENIO = None diff --git a/python/compress-image/test/1kb.png b/python/compress-image/test/1kb.png new file mode 100644 index 00000000..5c9c29ba Binary files /dev/null and b/python/compress-image/test/1kb.png differ diff --git a/python/compress-image/test/1kb_result_encoded_krakenio.txt b/python/compress-image/test/1kb_result_encoded_krakenio.txt new file mode 100644 index 00000000..263d0ef8 --- /dev/null +++ b/python/compress-image/test/1kb_result_encoded_krakenio.txt @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAABIAAAAUCAIAAAAP9fodAAABz0lEQVR42mP4TxZgoEjbtwMH3nV1vW1u/rh06d/PnyGCa8+8Kll1C4gWHX2GRdvLwsLHnp4PbWweWls/dnF5Ghr6+9kz155zhg0nVCqOApF+3XEgF0Xb1337gEofGBoioyN1faq5uyQLDyEjZDsZ3rS2PrK2RtO2xz9eN3sLmrbU+VcR2l7X1DyytETTdtA9RD9rE5q20GkXEdo+zp//yN4eTdvSmDSN3B3SERNVPdKdfHyT4ryq67IWrl6F0Pbn3bsnPj7Ievb52UT1hMlahqjomWqpKeioydoZyMQ5Sb9faPrjRCsiJH/dvftuwoSHyQlnA1yXhJkHdTjolFrIYYCV6VyfFxn+fnwIEW9AO0+e2Z6zINF5kpPhXBdlLy1MbZmuwh+nS/043Y2SSvY+OOK7Jl5/ngsQKdmpY2oLtRb7OEX4x+EqFG2nn19M2FoI0aYarIuprSZA8NNsxV+XZqNo+/Dj06QzcyHadDrt0PT4molfahb+tiv93+cn6En50ccncy8uz9hRHrohzbM5xCvc1c3eyMlMLc5N+Wi7wff9hX/eXseeA4B2Xn97++yLSxdfXjtx6dSp44eObl9y5cDSP0+P/v1wj0oZZ3BrAwARK/tUoZKzbgAAAABJRU5ErkJggg== \ No newline at end of file diff --git a/python/compress-image/test/1kb_result_encoded_tinypng.txt b/python/compress-image/test/1kb_result_encoded_tinypng.txt new file mode 100644 index 00000000..0f9d1e73 --- /dev/null +++ b/python/compress-image/test/1kb_result_encoded_tinypng.txt @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAABIAAAAUCAMAAAC3SZ14AAABLFBMVEX///8eHh7gMTEZccIvnkTiOjrw+PL87u6ixeb+8uF0qtrM6NJFjM4wf8gkeMX85MT2v78mbbkuarPujY3thITkSkrjRETiPT1AMyFeRB8rJh7ymR398/OtzOr75ubi8eT63d3X7dv87dfR6dZln9VVltHI0sq94MTKx8LJzLehq6qd0afUwKXxpaXFt6Txn5+SzJ360psbWJHEfo74y4v4yIXrfHz4w3pouHf3v3FgtXHpcXF7fmr2umclSGdVsGalXGZsoGHhY2BNrF+8T19ajFblVVVJg1TBR1QdOVRCTE1iXkpKV0XNUEUxnURDkkJSiEAsiT6+TjwsdTikVjfvoTUkLjVGPzLzojHFhzAlUy0jSioiPidNNhdVOxbxlBPSgxNpRRN8UBGpZwp9EYrKAAAAs0lEQVQY063KRXYCQBAA0UkyBHd3d3d3d3eH+9+B1z0cgAW1/K/Ip8mUqrCJELNAwHtLUa3laDoWkVgiEYvYo/mFkvI/CD8VB6mlQHIBZRj1GLmBolqkmjxRGQxLASCuGmTR7m8MBp1+nQazKRvj1Wz5gz13Uvz89enkyOhuTBHI26T0xOhyzSIJC5QeGD2McaT/GKVzlPOtzCeYNZirdkdbnX6fdyDAZxc6Pb5ISArPN3sB8REVVO/DWBYAAAAASUVORK5CYII= \ No newline at end of file diff --git a/python/compress-image/test_main.py b/python/compress-image/test_main.py new file mode 100644 index 00000000..0f25a284 --- /dev/null +++ b/python/compress-image/test_main.py @@ -0,0 +1,410 @@ +""" Unittests for Compress Image implementation in main.py.""" +# Standard library +import base64 +import pathlib +import unittest +from unittest.mock import patch + +# Third party +import requests +import tinify +from parameterized import parameterized + +# Local imports +import main +import secret + +# Path to 1kb image (png/jpg/jpeg) +IMAGE = pathlib.Path("test/1kb.png").read_bytes() + +# Path to tinypng encoded result (str) +RESULT_TINYPNG = ( + pathlib.Path("test/1kb_result_encoded_tinypng.txt"). + read_text(encoding="utf-8")) + +# Path to krakenio encoded result (str) +RESULT_KRAKENIO = ( + pathlib.Path("test/1kb_result_encoded_krakenio.txt"). + read_text(encoding="utf-8")) + + +class TestTinypng(unittest.TestCase): + """Class for testing the functionality of the "implement_tinypng" function.""" + @unittest.skipUnless(secret.API_KEY_TINYPNG, "No Tinypng API Key set.") + def test_tinypng_happy_path(self): + """Test case optimizing 1kb image using "implement_tinypng" function.""" + want = RESULT_TINYPNG + got = main.implement_tinypng( + { + "api_key": secret.API_KEY_TINYPNG, + "decoded_image": IMAGE, + } + ) + self.assertEqual(base64.b64encode(got).decode(), want) + + def test_tinypng_credential(self): + """Test case handling Account errors in the "implement_tinypng" function.""" + # Incorrect Credential + self.assertRaises( + tinify.errors.AccountError, + main.implement_tinypng, + { + "api_key": "1NCORRECT4CREDENT1ALS", + "decoded_image": IMAGE, + } + ) + + @unittest.skipUnless(secret.API_KEY_TINYPNG, "No Tinypng API Key set.") + @parameterized.expand([ + (b"",), + (b"ORw0KGgoAAAANSUhEUgAAABEAAAAOCAMAAAD+M",), + ]) + def test_tinypng_client(self, image): + """Test case for Client errors in the "implement_tinypng" function.""" + # Image is empty + data = { + "api_key": secret.API_KEY_TINYPNG, + "decoded_image": image, + } + self.assertRaises(tinify.errors.ClientError, main.implement_tinypng, data) + + @unittest.skipUnless(secret.API_KEY_TINYPNG, "No Tinypng API Key set.") + @parameterized.expand([ + ( + {"a": secret.API_KEY_TINYPNG, "decoded_image": IMAGE}, + ), + ( + {"api_key": secret.API_KEY_TINYPNG, "code_image": IMAGE}, + ), + ( + {"": secret.API_KEY_TINYPNG, "code_image": IMAGE}, + ), + ( + {"api_key": secret.API_KEY_TINYPNG, "": IMAGE}, + ), + ]) + def test_tinypng_keys(self, data): + """Test case for handling KeyError in the "implement_tinypng" function.""" + # Accessing wrong key + self.assertRaises( + KeyError, + main.implement_tinypng, + data, + ) + + @unittest.skipUnless(secret.API_KEY_TINYPNG, "No Tinypng API Key set.") + def test_tinypng_variables(self): + """Test case handling variables in the "implement_tinypng" function.""" + # Empty variables + self.assertRaises( + KeyError, + main.implement_tinypng, + {}, + ) + # One key in variable + self.assertRaises( + KeyError, + main.implement_tinypng, + { + "api_key": secret.API_KEY_TINYPNG + }, + ) + + @unittest.skipUnless(secret.API_KEY_TINYPNG, "No Tinypng API Key set.") + def test_implement_tinypng_basic_functionality_1kb(self): + """Basic functionality of "implement_tinypng" with a 1kb image.""" + with patch.object(tinify, "from_buffer") as mock_from_buffer: + # Set up the mock return value as decoded result + mock_from_buffer.return_value.to_buffer.return_value = ( + base64.b64decode(RESULT_TINYPNG)) + # Assert the expected result + optimized_image = main.implement_tinypng( + { + "api_key": secret.API_KEY_TINYPNG, + "decoded_image": IMAGE, + }, + ) + # Check if the return type is a byte + self.assertIsInstance(optimized_image, bytes) + # Check if the optimized_image equals mock_from_buffer return value + self.assertEqual( + optimized_image, + mock_from_buffer.return_value.to_buffer.return_value) + + @unittest.skipUnless(secret.API_KEY_TINYPNG, "No Tinypng API Key set.") + def test_implement_tinypng_unexpected_exception_account_error(self): + """Test case handling unexpected "AccountError" in implement_tinypng.""" + with patch.object(tinify, "from_buffer") as mock_from_buffer: + # Set up the mock return value as account exception + mock_from_buffer.side_effect = ( + tinify.errors.AccountError("API Key is wrong")) + # Check the raise for Account error + self.assertRaises( + tinify.errors.AccountError, + main.implement_tinypng, + { + "api_key": secret.API_KEY_TINYPNG, + "decoded_image": IMAGE, + }, + ) + + @unittest.skipUnless(secret.API_KEY_TINYPNG, "No Tinypng API Key set.") + def test_implement_tinypng_unexpected_exception_client_error(self): + """Test case handling unexpected "ClientError" in implement_tinypng.""" + with patch.object(tinify, "from_buffer") as mock_from_buffer: + # Set up the mock return value as client exception + mock_from_buffer.side_effect = ( + tinify.errors.ClientError("Image is incorrect.")) + # Check the raise for Client error + self.assertRaises( + tinify.errors.ClientError, + main.implement_tinypng, + { + "api_key": secret.API_KEY_TINYPNG, + "decoded_image": IMAGE + }, + ) + + +class TestKrakenIO(unittest.TestCase): + """Class for testing the functionality of the "implement_krakenio" function.""" + @unittest.skipUnless( + secret.API_KEY_KRAKENIO and secret.SECRET_API_KEY_KRAKENIO, + "No KrakenIO API Key or Secret Key.") + def test_krakenio_happy_path(self): + """Test case optimizing 1kb image using "implement_krakenio" function.""" + want = RESULT_KRAKENIO + got = main.implement_krakenio( + { + "api_key": secret.API_KEY_KRAKENIO, + "api_secret_key": secret.SECRET_API_KEY_KRAKENIO, + "decoded_image": IMAGE + }, + ) + self.assertEqual(got, base64.b64decode(want)) + + @unittest.skipUnless( + secret.API_KEY_KRAKENIO and secret.SECRET_API_KEY_KRAKENIO, + "No KrakenIO API Key or Secret Key.") + def test_krakenio_time_out(self): + """Test case for KrakenIO Read Timeout.""" + with patch.object(requests, "post") as mock_post: + mock_post.side_effect = requests.exceptions.ReadTimeout + self.assertRaises( + requests.exceptions.ReadTimeout, + main.implement_krakenio, + { + "api_key": secret.API_KEY_KRAKENIO, + "api_secret_key": secret.SECRET_API_KEY_KRAKENIO, + "decoded_image": IMAGE + }, + ) + + +class MyRequest: + """Class for defining My Request structure.""" + def __init__(self, data): + self.payload = data.get("payload", {}) + self.variables = data.get("variables", {}) + + +class MyResponse: + """Class for defining My Response structure.""" + def __init__(self): + self._json = None + + def json(self, data=None): + """Create a response for json.""" + if data is not None: + self._json = data + return self._json + + +class TestValidateRequest(unittest.TestCase): + """Class for unittests testing validating requests.""" + @parameterized.expand([ + [ + { + "payload": { + "provider": "tinypng", + "image": base64.b64encode(IMAGE).decode() + }, + "variables": {"API_KEY": secret.API_KEY_TINYPNG}, + }, + { + "provider": "tinypng", + "api_key": secret.API_KEY_TINYPNG, + "decoded_image": IMAGE + } + ], + [ + { + "payload": { + "provider": "krakenio", + "image": base64.b64encode(IMAGE).decode() + }, + "variables": { + "API_KEY": secret.API_KEY_KRAKENIO, + "SECRET_API_KEY": secret.SECRET_API_KEY_KRAKENIO + } + }, + { + "provider": "krakenio", + "api_key": secret.API_KEY_KRAKENIO, + "api_secret_key": secret.SECRET_API_KEY_KRAKENIO, + "decoded_image": IMAGE + } + ] + ]) + @unittest.skipUnless( + secret.API_KEY_KRAKENIO and + secret.SECRET_API_KEY_KRAKENIO and + secret.API_KEY_TINYPNG, + "No Tinypng Key and KrakenIO Key.", + ) + def test_validate_request(self, got, expected): + """Unittest for correct request.""" + req = MyRequest( + { + "payload": got["payload"], + "variables": got["variables"], + }, + ) + self.assertEqual(main.validate_request(req), expected) + + @parameterized.expand([ + [ + { + "payload": {}, + "variables": {}, + } + ], + [ + { + "payload": {"provider": "IMNOTAPROVIDER", "image": ""}, + "variables": {"API_KEY": "1234567"}, + } + ], + [ + { + "payload": {"provider": "krakenio", "image": ""}, + "variables": {}, + } + ], + [ + { + "payload": {"provider": "tinypng", "image": "12345"}, + "variables": {"API_KEY": ""}, + } + ], + [ + { + "payload": {"provider": "krakenio", "image": "12345"}, + "variables": {"API_KEY": "", "SECRET_API_KEY": ""}, + } + ], + [ + { + "payload": {"provider": "krakenio", "image": "12345"}, + "variables": {"API_KEY": "123", "SECRET_API_KEY": ""}, + } + ], + [ + { + "payload": {"WRONG_PROVIDER": "krakenio", "image": "12345"}, + "variables": {"API_KEY": "123", "SECRET_API_KEY": ""}, + } + ], + [ + { + "payload": {"provider": "krakenio", "1Mage": "12345"}, + "variables": {"API_KEY": "123", "SECRET_API_KEY": ""}, + } + ], + [ + { + "payload": {"provider": "krakenio", "1Mage": "12345"}, + "variables": {"NOT AN API": "123", "SECRET_API_KEY": ""}, + } + ], + [ + { + "payload": {"provider": "krakenio", "1Mage": "12345"}, + "variables": {"API": "123", "SecretKey": ""}, + } + ], + ]) + def test_validate_request_value_error(self, got): + """Unittest for testing value errors.""" + req = MyRequest( + { + "payload": got["payload"], + "variables": got["variables"], + }, + ) + self.assertRaises(ValueError, main.validate_request, req) + + +class TestMain(unittest.TestCase): + """Class test for main function.""" + @unittest.skipUnless(secret.API_KEY_TINYPNG, "No Tinypng API Key set.") + def test_main_success(self): + """Unittest for main function success json response.""" + want = { + "success": True, + "image": RESULT_TINYPNG + } + # Create a request + req = MyRequest({ + "payload": { + "provider": "tinypng", + "image": base64.b64encode(IMAGE).decode() + }, + "variables": { + "API_KEY": secret.API_KEY_TINYPNG + } + }) + # Create a response object + res = MyResponse() + main.main(req, res) + # Check the response + got = res.json() + self.assertEqual(got, want) + + def test_main_value_error(self): + """Unittest for main function when a value error is raised.""" + want = {"success": False, "error": "Missing payload."} + # Create a request + req = MyRequest({"payload": {}, "variables": {}}) + # Create a response object + res = MyResponse() + main.main(req, res) + + # Check the response + got = res.json() + self.assertEqual(got, want) + + def test_main_exception(self): + """Unittest case for main function when exception is raised.""" + # Create a request + req = MyRequest({ + "payload": { + "provider": "tinypng", + "image": base64.b64encode(IMAGE).decode() + }, + "variables": { + "API_KEY": "wrong_api_key" + } + }) + # Create a response object + res = MyResponse() # Create a response object + main.main(req, res) + + # Check the response + got = res.json() + self.assertFalse(got["success"]) + self.assertIn("AccountError", got["error"]) + + +if __name__ == "__main__": + unittest.main()