Fixes and cleanups #11

Merged
profitroll merged 7 commits from dev into master 2023-06-22 14:52:23 +03:00
16 changed files with 218 additions and 122 deletions

View File

@ -1,7 +1,9 @@
from typing import Literal from typing import Literal
from fastapi import HTTPException
class AlbumNotFoundError(Exception):
class AlbumNotFoundError(HTTPException):
"""Raises HTTP 404 if no album with this ID found.""" """Raises HTTP 404 if no album with this ID found."""
def __init__(self, id: str): def __init__(self, id: str):
@ -16,7 +18,7 @@ class AlbumNotFoundError(Exception):
} }
class AlbumNameNotFoundError(Exception): class AlbumNameNotFoundError(HTTPException):
"""Raises HTTP 404 if no album with this name found.""" """Raises HTTP 404 if no album with this name found."""
def __init__(self, name: str): def __init__(self, name: str):
@ -29,9 +31,15 @@ class AlbumNameNotFoundError(Exception):
} }
}, },
} }
super().__init__(
status_code=404,
detail=self.openapi["content"]["application/json"]["example"][
"detail"
].format(name=self.name),
)
class AlbumAlreadyExistsError(Exception): class AlbumAlreadyExistsError(HTTPException):
"""Raises HTTP 409 if album with this name already exists.""" """Raises HTTP 409 if album with this name already exists."""
def __init__(self, name: str): def __init__(self, name: str):
@ -44,9 +52,15 @@ class AlbumAlreadyExistsError(Exception):
} }
}, },
} }
super().__init__(
status_code=409,
detail=self.openapi["content"]["application/json"]["example"][
"detail"
].format(name=self.name),
)
class AlbumIncorrectError(Exception): class AlbumIncorrectError(HTTPException):
"""Raises HTTP 406 if album's title or name is invalid.""" """Raises HTTP 406 if album's title or name is invalid."""
def __init__(self, place: Literal["name", "title"], error: str) -> None: def __init__(self, place: Literal["name", "title"], error: str) -> None:
@ -56,13 +70,19 @@ class AlbumIncorrectError(Exception):
"description": "Album Name/Title Invalid", "description": "Album Name/Title Invalid",
"content": { "content": {
"application/json": { "application/json": {
"example": {"detail": "Album {name/title} invalid: {error}"} "example": {"detail": "Album {place} invalid: {error}"}
} }
}, },
} }
super().__init__(
status_code=406,
detail=self.openapi["content"]["application/json"]["example"][
"detail"
].format(place=self.place, error=self.error),
)
class PhotoNotFoundError(Exception): class PhotoNotFoundError(HTTPException):
"""Raises HTTP 404 if no photo with this ID found.""" """Raises HTTP 404 if no photo with this ID found."""
def __init__(self, id: str): def __init__(self, id: str):
@ -75,9 +95,15 @@ class PhotoNotFoundError(Exception):
} }
}, },
} }
super().__init__(
status_code=404,
detail=self.openapi["content"]["application/json"]["example"][
"detail"
].format(id=self.id),
)
class PhotoSearchQueryEmptyError(Exception): class PhotoSearchQueryEmptyError(HTTPException):
"""Raises HTTP 422 if no photo search query provided.""" """Raises HTTP 422 if no photo search query provided."""
def __init__(self): def __init__(self):
@ -91,9 +117,13 @@ class PhotoSearchQueryEmptyError(Exception):
} }
}, },
} }
super().__init__(
status_code=422,
detail=self.openapi["content"]["application/json"]["example"]["detail"],
)
class VideoNotFoundError(Exception): class VideoNotFoundError(HTTPException):
"""Raises HTTP 404 if no video with this ID found.""" """Raises HTTP 404 if no video with this ID found."""
def __init__(self, id: str): def __init__(self, id: str):
@ -106,9 +136,15 @@ class VideoNotFoundError(Exception):
} }
}, },
} }
super().__init__(
status_code=404,
detail=self.openapi["content"]["application/json"]["example"][
"detail"
].format(id=self.id),
)
class VideoSearchQueryEmptyError(Exception): class VideoSearchQueryEmptyError(HTTPException):
"""Raises HTTP 422 if no video search query provided.""" """Raises HTTP 422 if no video search query provided."""
def __init__(self): def __init__(self):
@ -122,9 +158,13 @@ class VideoSearchQueryEmptyError(Exception):
} }
}, },
} }
super().__init__(
status_code=422,
detail=self.openapi["content"]["application/json"]["example"]["detail"],
)
class SearchPageInvalidError(Exception): class SearchPageInvalidError(HTTPException):
"""Raises HTTP 400 if page or page size are not in valid range.""" """Raises HTTP 400 if page or page size are not in valid range."""
def __init__(self): def __init__(self):
@ -138,9 +178,13 @@ class SearchPageInvalidError(Exception):
} }
}, },
} }
super().__init__(
status_code=400,
detail=self.openapi["content"]["application/json"]["example"]["detail"],
)
class SearchTokenInvalidError(Exception): class SearchTokenInvalidError(HTTPException):
"""Raises HTTP 401 if search token is not valid.""" """Raises HTTP 401 if search token is not valid."""
def __init__(self): def __init__(self):
@ -150,9 +194,13 @@ class SearchTokenInvalidError(Exception):
"application/json": {"example": {"detail": "Invalid search token."}} "application/json": {"example": {"detail": "Invalid search token."}}
}, },
} }
super().__init__(
status_code=401,
detail=self.openapi["content"]["application/json"]["example"]["detail"],
)
class UserEmailCodeInvalid(Exception): class UserEmailCodeInvalid(HTTPException):
"""Raises HTTP 400 if email confirmation code is not valid.""" """Raises HTTP 400 if email confirmation code is not valid."""
def __init__(self): def __init__(self):
@ -164,9 +212,13 @@ class UserEmailCodeInvalid(Exception):
} }
}, },
} }
super().__init__(
status_code=400,
detail=self.openapi["content"]["application/json"]["example"]["detail"],
)
class UserAlreadyExists(Exception): class UserAlreadyExists(HTTPException):
"""Raises HTTP 409 if user with this name already exists.""" """Raises HTTP 409 if user with this name already exists."""
def __init__(self): def __init__(self):
@ -178,9 +230,13 @@ class UserAlreadyExists(Exception):
} }
}, },
} }
super().__init__(
status_code=409,
detail=self.openapi["content"]["application/json"]["example"]["detail"],
)
class AccessTokenInvalidError(Exception): class AccessTokenInvalidError(HTTPException):
"""Raises HTTP 401 if access token is not valid.""" """Raises HTTP 401 if access token is not valid."""
def __init__(self): def __init__(self):
@ -190,9 +246,13 @@ class AccessTokenInvalidError(Exception):
"application/json": {"example": {"detail": "Invalid access token."}} "application/json": {"example": {"detail": "Invalid access token."}}
}, },
} }
super().__init__(
status_code=401,
detail=self.openapi["content"]["application/json"]["example"]["detail"],
)
class UserCredentialsInvalid(Exception): class UserCredentialsInvalid(HTTPException):
"""Raises HTTP 401 if user credentials are not valid.""" """Raises HTTP 401 if user credentials are not valid."""
def __init__(self): def __init__(self):
@ -202,3 +262,7 @@ class UserCredentialsInvalid(Exception):
"application/json": {"example": {"detail": "Invalid credentials."}} "application/json": {"example": {"detail": "Invalid credentials."}}
}, },
} }
super().__init__(
status_code=401,
detail=self.openapi["content"]["application/json"]["example"]["detail"],
)

View File

@ -1,4 +1,5 @@
from typing import List, Union from typing import List, Union
from pydantic import BaseModel from pydantic import BaseModel

View File

@ -1,7 +1,5 @@
from fastapi import Request from fastapi import Request
from fastapi.responses import UJSONResponse from fastapi.responses import UJSONResponse
from modules.app import app
from classes.exceptions import *
from starlette.status import ( from starlette.status import (
HTTP_400_BAD_REQUEST, HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED, HTTP_401_UNAUTHORIZED,
@ -11,6 +9,23 @@ from starlette.status import (
HTTP_422_UNPROCESSABLE_ENTITY, HTTP_422_UNPROCESSABLE_ENTITY,
) )
from classes.exceptions import (
AlbumNotFoundError,
AlbumAlreadyExistsError,
AlbumIncorrectError,
PhotoNotFoundError,
PhotoSearchQueryEmptyError,
VideoNotFoundError,
VideoSearchQueryEmptyError,
SearchPageInvalidError,
SearchTokenInvalidError,
AccessTokenInvalidError,
UserEmailCodeInvalid,
UserAlreadyExists,
UserCredentialsInvalid,
)
from modules.app import app
@app.exception_handler(AlbumNotFoundError) @app.exception_handler(AlbumNotFoundError)
async def album_not_found_exception_handler(request: Request, exc: AlbumNotFoundError): async def album_not_found_exception_handler(request: Request, exc: AlbumNotFoundError):

View File

@ -1,31 +1,42 @@
from os import path from os import path
from modules.app import app
import aiofiles
from fastapi.responses import HTMLResponse, Response from fastapi.responses import HTMLResponse, Response
from modules.app import app
@app.get("/pages/matter.css", include_in_schema=False) @app.get("/pages/matter.css", include_in_schema=False)
async def page_matter(): async def page_matter():
with open(path.join("pages", "matter.css"), "r", encoding="utf-8") as f: async with aiofiles.open(
output = f.read() path.join("pages", "matter.css"), "r", encoding="utf-8"
) as f:
output = await f.read()
return Response(content=output) return Response(content=output)
@app.get("/pages/{page}/{file}", include_in_schema=False) @app.get("/pages/{page}/{file}", include_in_schema=False)
async def page_assets(page: str, file: str): async def page_assets(page: str, file: str):
with open(path.join("pages", page, file), "r", encoding="utf-8") as f: async with aiofiles.open(
output = f.read() path.join("pages", page, file), "r", encoding="utf-8"
) as f:
output = await f.read()
return Response(content=output) return Response(content=output)
@app.get("/", include_in_schema=False) @app.get("/", include_in_schema=False)
async def page_home(): async def page_home():
with open(path.join("pages", "home", "index.html"), "r", encoding="utf-8") as f: async with aiofiles.open(
output = f.read() path.join("pages", "home", "index.html"), "r", encoding="utf-8"
) as f:
output = await f.read()
return HTMLResponse(content=output) return HTMLResponse(content=output)
@app.get("/register", include_in_schema=False) @app.get("/register", include_in_schema=False)
async def page_register(): async def page_register():
with open(path.join("pages", "register", "index.html"), "r", encoding="utf-8") as f: async with aiofiles.open(
output = f.read() path.join("pages", "register", "index.html"), "r", encoding="utf-8"
) as f:
output = await f.read()
return HTMLResponse(content=output) return HTMLResponse(content=output)

View File

@ -1,15 +1,24 @@
import re import re
import pickle from datetime import datetime, timedelta, timezone
from os import makedirs, path, remove, system
from secrets import token_urlsafe from secrets import token_urlsafe
from shutil import move from shutil import move
from threading import Thread from threading import Thread
from typing import Union from typing import Union
from uuid import uuid4 from uuid import uuid4
from magic import Magic
from datetime import datetime, timedelta, timezone
from os import makedirs, path, remove, system
import aiofiles
from bson.errors import InvalidId
from bson.objectid import ObjectId
from fastapi import Security, UploadFile
from fastapi.responses import Response, UJSONResponse
from jose import JWTError, jwt
from magic import Magic
from plum.exceptions import UnpackError
from pydantic import ValidationError from pydantic import ValidationError
from pymongo import DESCENDING
from starlette.status import HTTP_204_NO_CONTENT, HTTP_409_CONFLICT
from classes.exceptions import ( from classes.exceptions import (
AccessTokenInvalidError, AccessTokenInvalidError,
AlbumNameNotFoundError, AlbumNameNotFoundError,
@ -19,8 +28,10 @@ from classes.exceptions import (
SearchTokenInvalidError, SearchTokenInvalidError,
) )
from classes.models import Photo, PhotoPublic, SearchResultsPhoto from classes.models import Photo, PhotoPublic, SearchResultsPhoto
from modules.app import app
from modules.database import col_albums, col_photos, col_tokens
from modules.exif_reader import extract_location from modules.exif_reader import extract_location
from modules.hasher import get_phash, get_duplicates from modules.hasher import get_duplicates, get_phash
from modules.scheduler import scheduler from modules.scheduler import scheduler
from modules.security import ( from modules.security import (
ALGORITHM, ALGORITHM,
@ -31,23 +42,6 @@ from modules.security import (
get_current_active_user, get_current_active_user,
get_user, get_user,
) )
from modules.app import app
from modules.database import col_photos, col_albums, col_tokens
from pymongo import DESCENDING
from bson.objectid import ObjectId
from bson.errors import InvalidId
from plum.exceptions import UnpackError
from jose import JWTError, jwt
from fastapi import UploadFile, Security
from fastapi.responses import UJSONResponse, Response
from fastapi.exceptions import HTTPException
from starlette.status import (
HTTP_204_NO_CONTENT,
HTTP_401_UNAUTHORIZED,
HTTP_409_CONFLICT,
)
from modules.utils import configGet, logWrite from modules.utils import configGet, logWrite
@ -130,7 +124,7 @@ async def photo_upload(
".".join(base_name) + f"_{int(datetime.now().timestamp())}." + extension ".".join(base_name) + f"_{int(datetime.now().timestamp())}." + extension
) )
with open( async with aiofiles.open(
path.join("data", "users", current_user.user, "albums", album, filename), "wb" path.join("data", "users", current_user.user, "albums", album, filename), "wb"
) as f: ) as f:
f.write(await file.read()) f.write(await file.read())
@ -266,16 +260,24 @@ if configGet("media_token_access") is True:
mime = Magic(mime=True).from_file(image_path) mime = Magic(mime=True).from_file(image_path)
with open(image_path, "rb") as f: async with aiofiles.open(image_path, "rb") as f:
image_file = f.read() image_file = await f.read()
return Response(image_file, media_type=mime) return Response(image_file, media_type=mime)
photo_get_responses = {404: PhotoNotFoundError("id").openapi} photo_get_responses = {
200: {"content": {"image/*": {}}},
404: PhotoNotFoundError("id").openapi,
}
@app.get("/photos/{id}", description="Get a photo by id", responses=photo_get_responses) @app.get(
"/photos/{id}",
description="Get a photo by id",
responses=photo_get_responses,
response_class=Response,
)
async def photo_get( async def photo_get(
id: str, id: str,
current_user: User = Security(get_current_active_user, scopes=["photos.read"]), current_user: User = Security(get_current_active_user, scopes=["photos.read"]),
@ -293,8 +295,8 @@ async def photo_get(
mime = Magic(mime=True).from_file(image_path) mime = Magic(mime=True).from_file(image_path)
with open(image_path, "rb") as f: async with aiofiles.open(image_path, "rb") as f:
image_file = f.read() image_file = await f.read()
return Response(image_file, media_type=mime) return Response(image_file, media_type=mime)

View File

@ -1,12 +1,10 @@
from datetime import timedelta from datetime import timedelta
from classes.exceptions import UserCredentialsInvalid
from modules.app import app
from fastapi import Depends from fastapi import Depends
from fastapi.security import ( from fastapi.security import OAuth2PasswordRequestForm
OAuth2PasswordRequestForm,
)
from classes.exceptions import UserCredentialsInvalid
from modules.app import app
from modules.security import ( from modules.security import (
ACCESS_TOKEN_EXPIRE_DAYS, ACCESS_TOKEN_EXPIRE_DAYS,
Token, Token,

View File

@ -1,27 +1,19 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from uuid import uuid1
from fastapi import Depends, Form
from fastapi.responses import Response, UJSONResponse
from starlette.status import HTTP_204_NO_CONTENT
from classes.exceptions import ( from classes.exceptions import (
UserAlreadyExists, UserAlreadyExists,
UserCredentialsInvalid, UserCredentialsInvalid,
UserEmailCodeInvalid, UserEmailCodeInvalid,
) )
from modules.database import (
col_users,
col_albums,
col_photos,
col_emails,
col_videos,
col_emails,
)
from modules.app import app from modules.app import app
from modules.utils import configGet, logWrite from modules.database import col_albums, col_emails, col_photos, col_users, col_videos
from modules.scheduler import scheduler
from modules.mailer import mail_sender from modules.mailer import mail_sender
from modules.scheduler import scheduler
from uuid import uuid1
from fastapi import Depends, Form
from fastapi.responses import Response, UJSONResponse
from starlette.status import HTTP_204_NO_CONTENT
from modules.security import ( from modules.security import (
User, User,
get_current_active_user, get_current_active_user,
@ -29,6 +21,7 @@ from modules.security import (
get_user, get_user,
verify_password, verify_password,
) )
from modules.utils import configGet, logWrite
async def send_confirmation(user: str, email: str): async def send_confirmation(user: str, email: str):

View File

@ -1,11 +1,19 @@
import re import re
import pickle from datetime import datetime, timezone
from os import makedirs, path, remove
from secrets import token_urlsafe from secrets import token_urlsafe
from shutil import move from shutil import move
from typing import Union from typing import Union
import aiofiles
from bson.errors import InvalidId
from bson.objectid import ObjectId
from fastapi import Security, UploadFile
from fastapi.responses import Response, UJSONResponse
from magic import Magic from magic import Magic
from datetime import datetime, timezone from pymongo import DESCENDING
from os import makedirs, path, remove from starlette.status import HTTP_204_NO_CONTENT
from classes.exceptions import ( from classes.exceptions import (
AlbumNameNotFoundError, AlbumNameNotFoundError,
SearchPageInvalidError, SearchPageInvalidError,
@ -13,17 +21,10 @@ from classes.exceptions import (
VideoNotFoundError, VideoNotFoundError,
VideoSearchQueryEmptyError, VideoSearchQueryEmptyError,
) )
from classes.models import Video, SearchResultsVideo, VideoPublic from classes.models import SearchResultsVideo, Video, VideoPublic
from modules.security import User, get_current_active_user
from modules.app import app from modules.app import app
from modules.database import col_videos, col_albums, col_tokens from modules.database import col_albums, col_tokens, col_videos
from bson.objectid import ObjectId from modules.security import User, get_current_active_user
from bson.errors import InvalidId
from pymongo import DESCENDING
from fastapi import UploadFile, Security
from fastapi.responses import UJSONResponse, Response
from starlette.status import HTTP_204_NO_CONTENT
video_post_responses = {404: AlbumNameNotFoundError("name").openapi} video_post_responses = {404: AlbumNameNotFoundError("name").openapi}
@ -59,10 +60,10 @@ async def video_upload(
".".join(base_name) + f"_{int(datetime.now().timestamp())}." + extension ".".join(base_name) + f"_{int(datetime.now().timestamp())}." + extension
) )
with open( async with aiofiles.open(
path.join("data", "users", current_user.user, "albums", album, filename), "wb" path.join("data", "users", current_user.user, "albums", album, filename), "wb"
) as f: ) as f:
f.write(await file.read()) await f.write(await file.read())
# Hashing and duplicates check should be here # Hashing and duplicates check should be here
@ -91,10 +92,18 @@ async def video_upload(
) )
video_get_responses = {404: VideoNotFoundError("id").openapi} video_get_responses = {
200: {"content": {"video/*": {}}},
404: VideoNotFoundError("id").openapi,
}
@app.get("/videos/{id}", description="Get a video by id", responses=video_get_responses) @app.get(
"/videos/{id}",
description="Get a video by id",
responses=video_get_responses,
response_class=Response,
)
async def video_get( async def video_get(
id: str, id: str,
current_user: User = Security(get_current_active_user, scopes=["videos.read"]), current_user: User = Security(get_current_active_user, scopes=["videos.read"]),
@ -112,10 +121,10 @@ async def video_get(
mime = Magic(mime=True).from_file(video_path) mime = Magic(mime=True).from_file(video_path)
with open(video_path, "rb") as f: async with aiofiles.open(video_path, "rb") as f:
video_file = f.read() video_file = await f.read()
return Response(video_file, media_type=mime) return Response(content=video_file, media_type=mime)
video_move_responses = {404: VideoNotFoundError("id").openapi} video_move_responses = {404: VideoNotFoundError("id").openapi}

View File

@ -1,6 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
app = FastAPI(title="END PLAY Photos", docs_url=None, redoc_url=None, version="0.2") app = FastAPI(title="END PLAY Photos", docs_url=None, redoc_url=None, version="0.2")

View File

@ -1,5 +1,6 @@
from pymongo import GEOSPHERE, MongoClient
from modules.utils import configGet from modules.utils import configGet
from pymongo import MongoClient, GEOSPHERE
db_config = configGet("database") db_config = configGet("database")

View File

@ -1,8 +1,9 @@
from modules.database import col_photos import cv2
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
from scipy import spatial from scipy import spatial
import cv2
from modules.database import col_photos
def hash_array_to_hash_hex(hash_array): def hash_array_to_hash_hex(hash_array):

View File

@ -1,6 +1,7 @@
from smtplib import SMTP, SMTP_SSL from smtplib import SMTP, SMTP_SSL
from traceback import print_exc
from ssl import create_default_context from ssl import create_default_context
from traceback import print_exc
from modules.utils import configGet, logWrite from modules.utils import configGet, logWrite
try: try:

View File

@ -1,16 +1,13 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import List, Union from typing import List, Union
from modules.database import col_users
from fastapi import Depends, HTTPException, Security, status from fastapi import Depends, HTTPException, Security, status
from fastapi.security import ( from fastapi.security import OAuth2PasswordBearer, SecurityScopes
OAuth2PasswordBearer,
SecurityScopes,
)
from jose import JWTError, jwt from jose import JWTError, jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from modules.database import col_users
with open("secret_key", "r", encoding="utf-8") as f: with open("secret_key", "r", encoding="utf-8") as f:
SECRET_KEY = f.read() SECRET_KEY = f.read()

View File

@ -1,6 +1,7 @@
from typing import Any, Union
from ujson import loads, dumps, JSONDecodeError
from traceback import print_exc from traceback import print_exc
from typing import Any, Union
from ujson import JSONDecodeError, dumps, loads
# Print to stdout and then to log # Print to stdout and then to log

View File

@ -1,10 +1,12 @@
from os import makedirs, path from os import makedirs, path
from modules.app import app
from modules.utils import *
from modules.scheduler import scheduler
from modules.extensions_loader import dynamic_import_from_src
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from modules.app import app
from modules.extensions_loader import dynamic_import_from_src
from modules.scheduler import scheduler
from modules.utils import *
makedirs(path.join("data", "users"), exist_ok=True) makedirs(path.join("data", "users"), exist_ok=True)

View File

@ -1,10 +1,11 @@
fastapi[all]==0.97.0 aiofiles==23.1.0
pymongo==4.4.0
ujson~=5.8.0
scipy~=1.10.1
python-magic~=0.4.27
opencv-python~=4.7.0.72
python-jose[cryptography]~=3.3.0
passlib~=1.7.4
apscheduler~=3.10.1 apscheduler~=3.10.1
exif==1.6.0 exif==1.6.0
fastapi[all]==0.97.0
opencv-python~=4.7.0.72
passlib~=1.7.4
pymongo==4.4.0
python-jose[cryptography]~=3.3.0
python-magic~=0.4.27
scipy~=1.10.1
ujson~=5.8.0