Skip to content

27 Update POST image/create endpoint #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 9, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: PR Pipeline
on: [pull_request]
on: [pull_request, workflow_dispatch]
jobs:
build:
name: Build
58 changes: 58 additions & 0 deletions examples/create_image.py
Original file line number Diff line number Diff line change
@@ -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}")
1 change: 1 addition & 0 deletions imaginate_api/config.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions imaginate_api/date/routes.py
Original file line number Diff line number Diff line change
@@ -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)
53 changes: 33 additions & 20 deletions imaginate_api/image/routes.py
Original file line number Diff line number Diff line change
@@ -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/<id>: 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/<id>: 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)
58 changes: 39 additions & 19 deletions imaginate_api/templates/index.html
Original file line number Diff line number Diff line change
@@ -5,24 +5,44 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Imaginate</title>
</head>
<body>
<form action="/create" method="post" enctype="multipart/form-data">
<label for="file">Choose file:</label>
<input type="file" name="file">
<br><br>
<label for="real">Real image:</label>
<select name="real" id="real">
<option value="true">True</option>
<option value="false">False</option>
</select>
<br><br>
<label for="date">Date:</label>
<input type="number" id="date" name="date" min="1">
<br><br>
<label for="theme">Theme:</label>
<input type="text" id="theme" name="theme">
<br><br>
<input type="submit" value="submit">
</form>
<body style="margin: 0; padding: 0;">
<div style="display: flex; width: 100%; height: 100vh; align-items: center; justify-content: space-around;">
<form action="/image/create" method="post" enctype="multipart/form-data">
<label for="file">Choose file:</label>
<input type="file" name="file">
<br><br>
<label for="real">Real image:</label>
<select name="real" id="real">
<option value="true">True</option>
<option value="false">False</option>
</select>
<br><br>
<label for="date">Date:</label>
<input type="number" id="date" name="date" min="1">
<br><br>
<label for="theme">Theme:</label>
<input type="text" id="theme" name="theme">
<br><br>
<input type="submit" value="submit">
</form>
<form action="/image/create" method="post" enctype="multipart/form-data">
<label for="url">URL:</label>
<input type="text" id="url" name="url">
<br><br>
<label for="real">Real image:</label>
<select name="real" id="real">
<option value="true">True</option>
<option value="false">False</option>
</select>
<br><br>
<label for="date">Date:</label>
<input type="number" id="date" name="date" min="1">
<br><br>
<label for="theme">Theme:</label>
<input type="text" id="theme" name="theme">
<br><br>
<input type="submit" value="submit">
</form>
</div>
</body>
</html>
67 changes: 63 additions & 4 deletions imaginate_api/utils.py
Original file line number Diff line number Diff line change
@@ -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,19 +27,74 @@ 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")
return res


# 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,
)
Loading
Loading