diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index b0784fe..94a5e1e 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -1,5 +1,5 @@ name: PR Pipeline -on: [pull_request] +on: [pull_request, workflow_dispatch] jobs: build: name: Build diff --git a/examples/create_image.py b/examples/create_image.py new file mode 100644 index 0000000..d36f241 --- /dev/null +++ b/examples/create_image.py @@ -0,0 +1,58 @@ +import requests +import mimetypes +from imaginate_api.schemas.image_info import ImageStatus +import os +from dotenv import load_dotenv + +# Quick script to test our POST image/create endpoint +# See also imaginate_api/templates/index.html for other ways to call the same endpoint +load_dotenv() + +# First type of call (via bytes) +IMAGE = "examples/images/pokemon.png" +URL = "http://127.0.0.1:5000/image/create" +MIME = mimetypes.guess_type(IMAGE) +PEXELS_BASE_URL = "https://api.pexels.com/v1" + +if not MIME: + print("Could not guess file type") + exit(1) +files = {"file": (IMAGE.split("/")[-1], open(IMAGE, "rb"), MIME[0])} +response = requests.post( + URL, + {"real": True, "date": 1, "theme": "pokemon", "status": ImageStatus.UNVERIFIED.value}, + files=files, +) +if response.ok: + print(f"Endpoint returned: {response.json()}") +else: + print(f"Endpoint returned: {response.status_code}") + +# Second type of call (via Pexels) +QUERY = "pokemon" +TOTAL_RESULTS = 1 # Max per page is 80: https://www.pexels.com/api/documentation/#photos-search__parameters__per_page +response = requests.get( + f"{PEXELS_BASE_URL}/search", + params={"query": QUERY, "per_page": TOTAL_RESULTS}, + headers={"Authorization": os.getenv("PEXELS_TOKEN")}, +) +response_data = response.json() +if TOTAL_RESULTS > response_data["total_results"]: + print(f"Requested {TOTAL_RESULTS} > Total {response_data['total_results']}") +photos_data = response_data["photos"] +for photo in photos_data: + response = requests.post( + URL, + { + "url": photo["src"]["original"], + "real": True, + "date": 1, + "theme": "pokemon", + "status": ImageStatus.UNVERIFIED.value, + }, + files=files, + ) + if response.ok: + print(f"Endpoint returned: {response.json()}") + else: + print(f"Endpoint returned: {response.status_code}") diff --git a/imaginate_api/config.py b/imaginate_api/config.py index 5744f5a..b6ae442 100644 --- a/imaginate_api/config.py +++ b/imaginate_api/config.py @@ -19,5 +19,6 @@ def get_db_env(): class Config: load_dotenv() MONGO_TOKEN = os.getenv("MONGO_TOKEN") + PEXELS_TOKEN = os.getenv("PEXELS_TOKEN") DB_ENV = get_db_env() TESTING = False diff --git a/imaginate_api/date/routes.py b/imaginate_api/date/routes.py index 1e61a27..94db222 100644 --- a/imaginate_api/date/routes.py +++ b/imaginate_api/date/routes.py @@ -1,6 +1,6 @@ from flask import Blueprint, abort, jsonify -from extensions import fs -from utils import build_result +from imaginate_api.extensions import fs +from imaginate_api.utils import build_result from http import HTTPStatus bp = Blueprint("date", __name__) @@ -24,6 +24,7 @@ def images_by_date(day): document.date, document.theme, document.status, + document.filename, ) ) return jsonify(out) diff --git a/imaginate_api/image/routes.py b/imaginate_api/image/routes.py index fdd8816..2f520bf 100644 --- a/imaginate_api/image/routes.py +++ b/imaginate_api/image/routes.py @@ -1,10 +1,17 @@ -from flask import Blueprint, abort, jsonify, make_response, request +from flask import Blueprint, jsonify, make_response, request from imaginate_api.extensions import fs from image_handler_client.schemas.image_info import ImageStatus -from imaginate_api.utils import str_to_bool, validate_id, search_id, build_result -from http import HTTPStatus +from imaginate_api.utils import ( + validate_id, + search_id, + build_result, + validate_post_image_create_request, + build_image_from_url, +) + bp = Blueprint("image", __name__) + # GET /read: used for viewing all images # This endpoint is simply for testing purposes @bp.route("/read") @@ -19,19 +26,22 @@ def read_all(): # TODO: Add a lot of validation to fit our needs @bp.route("/create", methods=["POST"]) def upload(): - try: - file = request.files["file"] - date = int(request.form["date"]) - theme = request.form["theme"] - real = str_to_bool(request.form["real"]) - status = ImageStatus.UNVERIFIED.value - except (KeyError, TypeError, ValueError): - abort(HTTPStatus.BAD_REQUEST, description="Invalid schema") - if not ( - file.filename and file.content_type and file.content_type.startswith("image/") - ): - abort(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, description="Invalid file") - + request_url = request.form.get( + "url", None + ) # Determine if our request wants us to use a url + if request_url: + print("Getting file data through url attribute") + request_file = build_image_from_url(request_url) + else: + print("Getting file data through file attribute") + request_file = request.files.get("file") + file, date, theme, real = validate_post_image_create_request( + request_file, + request.form.get("date"), + request.form.get("theme"), + request.form.get("real"), + ) + status = ImageStatus.UNVERIFIED.value _id = fs.put( file.stream.read(), filename=file.filename, @@ -41,7 +51,7 @@ def upload(): real=real, status=status, ) - return jsonify(build_result(_id, real, date, theme, status)) + return jsonify(build_result(_id, real, date, theme, status, file.filename)) # GET /read/: used for viewing a specific image @@ -61,7 +71,9 @@ def read(id): def read_properties(id): _id = validate_id(id) res = search_id(_id) - return jsonify(build_result(res._id, res.real, res.date, res.theme, res.status)) + return jsonify( + build_result(res._id, res.real, res.date, res.theme, res.status, res.filename) + ) # DELETE /delete/: used for deleting a single image @@ -74,7 +86,8 @@ def delete_image(id): res_date = getattr(res, "date", None) res_theme = getattr(res, "theme", None) res_status = getattr(res, "status", None) + res_filename = getattr(res, "filename", None) - info = build_result(res._id, res_real, res_date, res_theme, res_status) + info = build_result(res._id, res_real, res_date, res_theme, res_status, res_filename) fs.delete(res._id) - return info + return jsonify(info) diff --git a/imaginate_api/templates/index.html b/imaginate_api/templates/index.html index 22ad10d..df4f709 100644 --- a/imaginate_api/templates/index.html +++ b/imaginate_api/templates/index.html @@ -5,24 +5,44 @@ Imaginate - -
- - -

- - -

- - -

- - -

- -
+ +
+
+ + +

+ + +

+ + +

+ + +

+ +
+
+ + +

+ + +

+ + +

+ + +

+ +
+
diff --git a/imaginate_api/utils.py b/imaginate_api/utils.py index 1c7c571..fc9f638 100644 --- a/imaginate_api/utils.py +++ b/imaginate_api/utils.py @@ -1,8 +1,12 @@ -from flask import abort +from flask import abort, current_app from bson.errors import InvalidId from bson.objectid import ObjectId from http import HTTPStatus from imaginate_api.extensions import fs +import requests +from werkzeug.datastructures import FileStorage +from io import BytesIO +from urllib.parse import urlparse # Helper function to get boolean @@ -23,7 +27,6 @@ def validate_id(image_id: str | ObjectId | bytes): # Helper function to search MongoDB ID def search_id(_id: ObjectId): - print(fs) res = next(fs.find({"_id": _id}), None) if not res: abort(HTTPStatus.NOT_FOUND, "Collection not found") @@ -31,11 +34,67 @@ def search_id(_id: ObjectId): # Helper function to build schema-matching JSON response -def build_result(_id: ObjectId, real: bool, date: int, theme: str, status: str): +def build_result( + _id: ObjectId, real: bool, date: int, theme: str, status: str, filename: str +): return { - "url": "/read/" + str(_id), + "filename": filename, + "url": "image/read/" + str(_id), "real": real, "date": date, "theme": theme, "status": status, } + + +# Helper function to validate POST image/create endpoint +def validate_post_image_create_request(file, date, theme, real): + if any(x is None for x in [file, date, theme, real]): + abort(HTTPStatus.BAD_REQUEST, description="Invalid schema") + try: + date = int(date) + real = str_to_bool(real) + except (KeyError, TypeError, ValueError): + abort(HTTPStatus.BAD_REQUEST, description="Invalid schema") + if not ( + file.filename and file.content_type and file.content_type.startswith("image/") + ): + abort(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, description="Invalid file") + return file, date, theme, real + + +# This catches most urls but not all! +def validate_url(url): + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) + except AttributeError: + return False + + +def build_image_from_url(url): + if not validate_url(url): + abort(HTTPStatus.BAD_REQUEST, description=f"Malformed URL: {url}") + + # Get raw content of the source image + photo_response = requests.get( + url, headers={"Authorization": current_app.config["PEXELS_TOKEN"]}, stream=True + ) + if not photo_response.ok: + abort( + photo_response.status_code, + description=f"Request to URL failed with: {photo_response.reason}", + ) + raw_photo = photo_response.content + type_photo = photo_response.headers.get("Content-Type") + if not (type_photo and type_photo.startswith("image/")): + abort( + HTTPStatus.UNSUPPORTED_MEDIA_TYPE, description=f"Invalid file from URL: {url}" + ) + + # Return content using file storage + return FileStorage( + stream=BytesIO(raw_photo), + filename=str(url).rstrip("/").split("/")[-1], + content_type=type_photo, + ) diff --git a/poetry.lock b/poetry.lock index 207280d..3b54ed7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,17 @@ files = [ {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, ] +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -22,6 +33,105 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -130,6 +240,17 @@ files = [ [package.extras] license = ["ukkonen"] +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + [[package]] name = "image-handler-client" version = "0.1.0" @@ -530,6 +651,27 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "ruff" version = "0.5.7" @@ -583,6 +725,23 @@ core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.te doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "virtualenv" version = "20.26.3" @@ -623,4 +782,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "fc46cf81a641e44aef26fce655f3a5fa1db3a4965f623bcd6bb80a5a41978d94" +content-hash = "53dbf0dfb04702ee2e2cb0709e89023d4c2334b63806ee57a992723d7fc35576" diff --git a/pyproject.toml b/pyproject.toml index ffa2a7c..78387e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ pytest = "^8.2.2" mongomock = "^4.1.2" setuptools = "^72.1.0" image-handler-client = {git = "https://github.com/imaginate-ai/image-handler-client.git", rev = "v1.0.0"} +requests = "^2.32.3" [tool.ruff] exclude = [] diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 766c07d..07bc014 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -15,9 +15,10 @@ from bson.objectid import ObjectId from io import BytesIO from http import HTTPStatus +from image_handler_client.schemas.image_info import ImageStatus # Mocking -from unittest.mock import patch +from unittest.mock import patch, MagicMock import mongomock # NOTE: mongomock doesn't work well with sorting @@ -59,12 +60,12 @@ def mock_data(): mock_data = [ { "data": b"data", - "filename": "sample", + "filename": f"sample-{i}", "type": "image/png", "date": i, "theme": "sample", "real": True, - "status": "unverified", + "status": ImageStatus.UNVERIFIED.value, } for i in range(5) ] @@ -73,7 +74,7 @@ def mock_data(): # Set up database before running tests @pytest.fixture(autouse=True) -def setup(mock_db, mock_fs): +def setup(mock_fs): with ( patch("imaginate_api.date.routes.fs", mock_fs), patch("imaginate_api.image.routes.fs", mock_fs), @@ -153,30 +154,52 @@ def test_post_image_create_endpoint_success(client, mock_data): entry["date"], entry["theme"], entry["status"], + entry["filename"], ) @pytest.mark.parametrize( "data, expected", [ - ({}, HTTPStatus.BAD_REQUEST), + ( + {"file": FileStorage(stream=BytesIO(b"data"), content_type="image/png")}, + HTTPStatus.BAD_REQUEST, + ), ( { "date": 0, "theme": "sample", "real": True, - "status": "unverified", + "status": ImageStatus.UNVERIFIED.value, "file": FileStorage( stream=BytesIO(b"data"), filename="test.pdf", content_type="application/pdf" ), }, HTTPStatus.UNSUPPORTED_MEDIA_TYPE, ), + ( + { + "date": 0, + "theme": "sample", + "real": True, + "status": ImageStatus.UNVERIFIED.value, + "url": "https://www.google.com", # Causes endpoint to take a different flow + "file": FileStorage( + stream=BytesIO(b"data"), filename="test.png", content_type="image/png" + ), + }, + HTTPStatus.OK, + ), ], ) -def test_post_image_create_endpoint_exception(data, expected, client): - res = client.post("/image/create", data=data, content_type="multipart/form-data") - assert res.status_code == expected +def test_post_image_create_endpoint_status_code(data, expected, client): + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.content = data["file"].stream.read() + mock_response.headers.get = MagicMock(return_value=data["file"].content_type) + mock_get.return_value = mock_response + res = client.post("/image/create", data=data, content_type="multipart/form-data") + assert res.status_code == expected # Not testing scenarios with exceptions as they have been tested by helper functions: @@ -197,7 +220,12 @@ def test_get_image_read_properties_endpoint(mock_fs, mock_data, client): res = client.get(f"/image/read/{_id}/properties") assert res.status_code == HTTPStatus.OK assert res.json == build_result( - _id, entry["real"], entry["date"], entry["theme"], entry["status"] + _id, + entry["real"], + entry["date"], + entry["theme"], + entry["status"], + entry["filename"], ) @@ -207,7 +235,14 @@ def test_get_date_images_endpoint_success(mock_fs, mock_data, client): res = client.get(f"/date/{entry['date']}/images") assert res.status_code == HTTPStatus.OK assert res.json == [ - build_result(_id, entry["real"], entry["date"], entry["theme"], entry["status"]) + build_result( + _id, + entry["real"], + entry["date"], + entry["theme"], + entry["status"], + entry["filename"], + ) ] @@ -245,7 +280,12 @@ def test_delete_image_endpoint(mock_fs, mock_data, client): res = client.delete(f"/image/{_id}") assert res.status_code == HTTPStatus.OK assert res.json == build_result( - _id, entry["real"], entry["date"], entry["theme"], entry["status"] + _id, + entry["real"], + entry["date"], + entry["theme"], + entry["status"], + entry["filename"], )