Video tests pass with updated Sanic.

This commit is contained in:
Roland 2022-01-31 21:18:49 -07:00
parent b1ed453466
commit c277a75a1a
No known key found for this signature in database
GPG key ID: 9D6DBD3ACF11AB93
10 changed files with 72 additions and 98 deletions

View file

@ -103,9 +103,9 @@ async def refresh_videos(channel_links: list = None):
@validate(FavoriteRequest)
@openapi.response(HTTPStatus.OK, FavoriteResponse)
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
async def favorite(_: Request, data: dict):
_favorite = video_lib.set_video_favorite(data['video_id'], data['favorite'])
ret = {'video_id': data['video_id'], 'favorite': _favorite}
async def favorite(_: Request, body: FavoriteRequest):
_favorite = video_lib.set_video_favorite(body.video_id, body.favorite)
ret = {'video_id': body.video_id, 'favorite': _favorite}
return json_response(ret, HTTPStatus.OK)

View file

@ -12,7 +12,7 @@ from wrolpi.root_api import json_response
from wrolpi.schema import JSONErrorResponse
from .lib import get_minimal_channels, delete_channel, update_channel, get_channel, create_channel
from ..schema import ChannelsResponse, ChannelResponse, ChannelPostRequest, \
ChannelPostResponse, ChannelPutRequest, SuccessResponse
ChannelPostResponse, ChannelPutRequest
channel_bp = Blueprint('Channel', url_prefix='/api/videos/channels')
@ -27,7 +27,7 @@ async def get_channels(_: Request):
return json_response({'channels': channels})
@channel_bp.route('/<link:str>', methods=['GET', 'OPTIONS'])
@channel_bp.get('/<link:str>')
@openapi.description('Get a Channel')
@openapi.response(HTTPStatus.OK, ChannelResponse)
def channel_get(_: Request, link: str):
@ -42,14 +42,14 @@ def channel_get(_: Request, link: str):
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
@validate(ChannelPostRequest)
@wrol_mode_check
def channel_post(_: Request, data: dict):
def channel_post(_: Request, body: ChannelPostRequest):
try:
# Channel directory is relative to the media directory. Channel directory may not be in "videos" directory!
data['directory'] = get_media_directory() / data['directory']
body.directory = get_media_directory() / body.directory
except UnknownDirectory:
if data['mkdir']:
make_media_directory(data['directory'])
data['directory'] = get_relative_to_media_directory(data['directory'])
if body.mkdir:
make_media_directory(body.directory)
body.directory = get_relative_to_media_directory(body.directory)
else:
raise
# Channel directory is relative to the media directory. Channel directory may not be in "videos" directory!
@ -57,16 +57,7 @@ def channel_post(_: Request, data: dict):
if data.get('mkdir') is True:
make_media_directory(data['directory'])
if download_frequency := data.get('download_frequency'):
try:
download_frequency = int(download_frequency)
except ValueError:
if download_frequency in ('null', 'None'):
download_frequency = None
data['download_frequency'] = download_frequency
channel = create_channel(data)
channel = create_channel(body.__dict__) # TODO don't use the dataclass as a dict.
# Refresh the videos asynchronously
from ..api import refresh_videos
@ -78,14 +69,13 @@ def channel_post(_: Request, data: dict):
@channel_bp.put('/<link:str>')
@channel_bp.patch('/<link:str>')
@openapi.description('Update a Channel')
@openapi.response(HTTPStatus.NO_CONTENT)
@openapi.response(HTTPStatus.BAD_REQUEST, JSONErrorResponse)
@validate(ChannelPutRequest)
@wrol_mode_check
def channel_update(_: Request, link: str, data: dict):
channel = update_channel(data, link)
def channel_update(_: Request, link: str, body: ChannelPutRequest):
channel = update_channel(body.__dict__, link)
return response.raw('', HTTPStatus.NO_CONTENT,
headers={'Location': f'/api/videos/channels/{channel.link}'})
@ -93,7 +83,6 @@ def channel_update(_: Request, link: str, data: dict):
@channel_bp.delete('/<link:str>')
@openapi.description('Delete a Channel')
@openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse)
@validate(SuccessResponse)
@wrol_mode_check
def channel_delete(_: Request, link: str):
delete_channel(link)

View file

@ -139,25 +139,18 @@ def update_channel(data: dict, link: str) -> Channel:
if 'directory' in data:
data['directory'] = str(data['directory'])
if 'download_frequency' in data:
try:
data['download_frequency'] = int(data['download_frequency'])
except ValueError:
if data['download_frequency'] in ('null', 'None', ''):
data['download_frequency'] = None
else:
raise APIError(f'Invalid download frequency {data["download_frequency"]}')
if data.get('match_regex') in ('None', 'null'):
data['match_regex'] = None
data['link'] = data.get('link') or link
# Verify that the URL/Name/Link aren't taken
check_for_channel_conflicts(
session,
id_=channel.id,
url=data.get('url'),
name=data.get('name'),
link=data.get('link'),
link=data['link'],
directory=data.get('directory'),
)

View file

@ -60,7 +60,7 @@ class TestVideoAPI(TestAPI):
# Create it
request, response = api_app.test_client.post('/api/videos/channels', content=json.dumps(new_channel))
assert response.status_code == HTTPStatus.CREATED
assert response.status_code == HTTPStatus.CREATED, response.json
location = response.headers['Location']
request, response = api_app.test_client.get(location)
created = response.json['channel']
@ -94,20 +94,6 @@ class TestVideoAPI(TestAPI):
'url': 'https://example.com/channel1',
})
# Patch it
patch = {'name': 'new name'}
request, response = api_app.test_client.patch(location, content=json.dumps(patch))
assert response.status_code == HTTPStatus.NO_CONTENT, response.status_code
request, response = api_app.test_client.get(location)
assert response.status_code == HTTPStatus.OK
self.assertDictContains(response.json['channel'], {
'id': 1,
'name': 'new name',
'directory': channel_directory_name,
'match_regex': 'asdf',
'url': 'https://example.com/channel1',
})
# Can't update channel that doesn't exist
request, response = api_app.test_client.put('/api/videos/channels/doesnt_exist',
content=json.dumps(new_channel))
@ -361,9 +347,7 @@ class TestVideoAPI(TestAPI):
@wrap_test_db
def test_channel_no_download_frequency(self):
"""
A channel does not require a download frequency.
"""
"""A channel does not require a download frequency."""
channel_directory = tempfile.TemporaryDirectory(dir=self.tmp_dir.name).name
os.mkdir(channel_directory)

View file

@ -2,30 +2,33 @@ from dataclasses import dataclass, field
from datetime import date, datetime
from typing import List, Optional
from modules.videos.lib import DEFAULT_DOWNLOAD_FREQUENCY
from modules.videos.video.lib import DEFAULT_VIDEO_ORDER, VIDEO_QUERY_LIMIT
@dataclass
class ChannelPostRequest:
url: str
name: str
match_regex: str
directory: str
mkdir: bool
generate_posters: bool
calculate_duration: bool
download_frequency: str
calculate_duration: Optional[bool] = None
download_frequency: Optional[int] = DEFAULT_DOWNLOAD_FREQUENCY
generate_posters: Optional[bool] = None
match_regex: Optional[str] = None
mkdir: Optional[bool] = None
url: Optional[str] = None
@dataclass
class ChannelPutRequest:
url: str
name: str
match_regex: str
link: str
directory: str
generate_posters: bool
calculate_duration: bool
mkdir: bool
download_frequency: str
calculate_duration: Optional[bool] = None
directory: Optional[str] = None
download_frequency: Optional[int] = DEFAULT_DOWNLOAD_FREQUENCY
generate_posters: Optional[bool] = None
link: Optional[str] = None
match_regex: Optional[str] = None
mkdir: Optional[bool] = None
name: Optional[str] = None
url: Optional[str] = None
@dataclass
@ -33,11 +36,6 @@ class RefreshRequest:
channel_name: str
@dataclass
class SuccessResponse:
success: str
@dataclass
class ChannelModel:
id: int
@ -129,11 +127,11 @@ class VideoResponse:
@dataclass
class VideoSearchRequest:
search_str: str
filters: Optional[List[str]] = field(default_factory=list)
search_str: Optional[str] = None
filters: List[str] = field(default_factory=list)
offset: Optional[int] = None
limit: Optional[int] = None
order_by: Optional[str] = None
limit: Optional[int] = VIDEO_QUERY_LIMIT
order_by: Optional[str] = DEFAULT_VIDEO_ORDER
channel_link: Optional[str] = None

View file

@ -8,11 +8,11 @@ from sanic_ext.extensions.openapi import openapi
from wrolpi.common import logger, wrol_mode_check, run_after, get_media_directory, \
get_relative_to_media_directory
from wrolpi.db import get_db_session
from wrolpi.errors import ValidationError, InvalidOrderBy
from wrolpi.errors import InvalidOrderBy
from wrolpi.root_api import json_response
from wrolpi.schema import JSONErrorResponse
from .lib import get_video, VIDEO_ORDERS, DEFAULT_VIDEO_ORDER, video_search, get_video_for_app
from ..common import get_matching_directories, video_limiter
from .lib import get_video, VIDEO_ORDERS, video_search, get_video_for_app
from ..common import get_matching_directories
from ..lib import save_channels_config
from ..schema import VideoResponse, VideoSearchRequest, VideoSearchResponse, \
DirectoriesResponse, DirectoriesRequest
@ -36,21 +36,18 @@ def video_get(_: Request, video_id: int):
@openapi.response(HTTPStatus.OK, VideoSearchResponse)
@openapi.response(HTTPStatus.NOT_FOUND, JSONErrorResponse)
@validate(VideoSearchRequest)
async def search(_: Request, data: dict):
try:
search_str = data.get('search_str')
channel_link = data.get('channel_link')
order_by = data.get('order_by', DEFAULT_VIDEO_ORDER)
offset = int(data.get('offset', 0))
limit = video_limiter(data.get('limit'))
filters = data.get('filters', None)
except Exception as e:
raise ValidationError('Unable to validate search queries') from e
if order_by not in VIDEO_ORDERS:
async def search(_: Request, body: VideoSearchRequest):
if body.order_by not in VIDEO_ORDERS:
raise InvalidOrderBy('Invalid order by')
videos, videos_total = video_search(search_str, offset, limit, channel_link, order_by, filters)
videos, videos_total = video_search(
body.search_str,
body.offset,
body.limit,
body.channel_link,
body.order_by,
body.filters,
)
ret = {'videos': list(videos), 'totals': {'videos': videos_total}}
return json_response(ret)

View file

@ -91,8 +91,6 @@ class TestVideoFunctions(TestAPI):
self.assertError(resp, HTTPStatus.FORBIDDEN, API_ERRORS[WROLModeEnabled]['code'])
_, resp = api_app.test_client.put('/api/videos/channels/channel1', content=channel)
self.assertError(resp, HTTPStatus.FORBIDDEN, API_ERRORS[WROLModeEnabled]['code'])
_, resp = api_app.test_client.patch('/api/videos/channels/channel1', content=channel)
self.assertError(resp, HTTPStatus.FORBIDDEN, API_ERRORS[WROLModeEnabled]['code'])
_, resp = api_app.test_client.delete('/api/videos/channels/channel1')
self.assertError(resp, HTTPStatus.FORBIDDEN, API_ERRORS[WROLModeEnabled]['code'])

View file

@ -10,7 +10,7 @@ pytest==6.2.5
pytz==2021.3
PyYAML==5.3.1
requests==2.27.1
sanic-ext==21.12.1
sanic-ext==22.1.2
sanic-testing==0.8.2
sanic==21.12.1
selenium==4.1.0

View file

@ -458,7 +458,7 @@ def get_absolute_media_path(path: str) -> Path:
return path
def get_relative_to_media_directory(path: str) -> Path:
def get_relative_to_media_directory(path: Union[str, Path]) -> Path:
"""
Get the path for a file/directory relative to the config media directory.
@ -482,7 +482,7 @@ def minimize_dict(d: dict, keys: Iterable) -> Optional[dict]:
return {k: d[k] for k in set(keys) & d.keys()}
def make_media_directory(path: str):
def make_media_directory(path: Union[str, Path]):
"""
Make a directory relative within the media directory.
"""

View file

@ -18,7 +18,7 @@ from wrolpi.common import set_sanic_url_parts, logger, get_config, wrol_mode_ena
Base, get_media_directory, wrol_mode_check
from wrolpi.dates import set_timezone
from wrolpi.downloader import download_manager
from wrolpi.errors import WROLModeEnabled, InvalidTimezone
from wrolpi.errors import WROLModeEnabled, InvalidTimezone, API_ERRORS, APIError, ValidationError
from wrolpi.media_path import MediaPath
from wrolpi.schema import RegexRequest, RegexResponse, SettingsRequest, SettingsResponse, DownloadRequest, EchoResponse
from wrolpi.vars import DOCKERIZED
@ -250,3 +250,18 @@ def json_response(*a, **kwargs) -> HTTPResponse:
"""
resp = response.json(*a, **kwargs, cls=CustomJSONEncoder, dumps=json.dumps)
return resp
def json_error_handler(request, exception: Exception):
error = API_ERRORS[type(exception)]
if isinstance(exception, ValidationError):
body = dict(error='Could not validate the contents of the request', code=error['code'])
else:
body = dict(message=str(exception), api_error=error['message'], code=error['code'])
if cause := exception.__cause__:
cause = API_ERRORS[type(cause)]
body['cause'] = dict(error=cause['message'], code=cause['code'])
return json_response(body, error['status'])
api_app.error_handler.add(APIError, json_error_handler)