mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Moving config api endpoints to their own file..
This commit is contained in:
parent
fc084e2421
commit
376850ceeb
10 changed files with 190 additions and 139 deletions
|
|
@ -327,7 +327,7 @@ export async function getDownloads() {
|
|||
}
|
||||
|
||||
export async function getConfigs() {
|
||||
const response = await apiGet(`${API_URI}/configs`);
|
||||
const response = await apiGet(`${API_URI}/config`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return {
|
||||
|
|
@ -347,7 +347,7 @@ export async function getConfigs() {
|
|||
|
||||
export async function postImportConfig(fileName) {
|
||||
const body = {file_name: fileName};
|
||||
const response = await apiPost(`${API_URI}/configs/import`, body);
|
||||
const response = await apiPost(`${API_URI}/config/import`, body);
|
||||
if (!response.ok) {
|
||||
const message = await getErrorMessage(response, 'Could not import config.');
|
||||
toast({
|
||||
|
|
@ -360,14 +360,14 @@ export async function postImportConfig(fileName) {
|
|||
return response
|
||||
}
|
||||
|
||||
export async function postSaveConfig(fileName) {
|
||||
export async function postDumpConfig(fileName) {
|
||||
const body = {file_name: fileName, overwrite: true};
|
||||
const response = await apiPost(`${API_URI}/configs/save`, body);
|
||||
const response = await apiPost(`${API_URI}/config/dump`, body);
|
||||
if (!response.ok) {
|
||||
const message = await getErrorMessage(response, 'Could not save config.');
|
||||
const message = await getErrorMessage(response, 'Could not dump config.');
|
||||
toast({
|
||||
type: 'error',
|
||||
title: 'Saving config Failed',
|
||||
title: 'Dumping config failed',
|
||||
description: message,
|
||||
time: 5000,
|
||||
});
|
||||
|
|
@ -375,19 +375,27 @@ export async function postSaveConfig(fileName) {
|
|||
return response
|
||||
}
|
||||
|
||||
export async function fetchVideoDownloaderConfig() {
|
||||
const response = await apiGet(`${API_URI}/config/videos_downloader.yaml`);
|
||||
export async function getConfig(fileName) {
|
||||
const response = await apiGet(`${API_URI}/config?file_name=${fileName}`);
|
||||
return (await response.json())['config'];
|
||||
}
|
||||
|
||||
export async function saveVideoDownloaderConfig(config) {
|
||||
export async function fetchVideoDownloaderConfig() {
|
||||
return await getConfig('videos_downloader.yaml');
|
||||
}
|
||||
|
||||
export async function updateConfig(fileName, config) {
|
||||
const body = {config};
|
||||
const response = await apiPost(`${API_URI}/config/videos_downloader.yaml`, body);
|
||||
const response = await apiPost(`${API_URI}/config?file_name=${fileName}`, body);
|
||||
if (!response.ok) {
|
||||
throw Error('Failed to save config');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateVideoDownloaderConfig(config) {
|
||||
return await updateConfig('videos_downloader.yaml', config);
|
||||
}
|
||||
|
||||
export async function validateRegex(regex) {
|
||||
const response = await apiPost(`${API_URI}/valid_regex`, {regex});
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import {FileRowTagIcon, FilesView} from "./Files";
|
|||
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
|
||||
import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
|
||||
import {Button, Card, Header, Loader, Placeholder, Popup, Segment, Statistic, StatisticGroup} from "./Theme";
|
||||
import {deleteVideos, fetchVideoDownloaderConfig, postVideoFileFormat, saveVideoDownloaderConfig} from "../api";
|
||||
import {deleteVideos, fetchVideoDownloaderConfig, postVideoFileFormat, updateVideoDownloaderConfig} from "../api";
|
||||
import {Media, QueryContext, ThemeContext} from "../contexts/contexts";
|
||||
import _ from "lodash";
|
||||
import {defaultFileOrder, defaultSearchOrder} from "./Vars";
|
||||
|
|
@ -264,7 +264,7 @@ function VideosSettings() {
|
|||
}
|
||||
|
||||
const submitter = async () => {
|
||||
return await saveVideoDownloaderConfig(form.formData);
|
||||
return await updateVideoDownloaderConfig(form.formData);
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
getVideo,
|
||||
getVideosStatistics,
|
||||
postImportConfig,
|
||||
postSaveConfig,
|
||||
postDumpConfig,
|
||||
saveSettings,
|
||||
searchArchives,
|
||||
searchChannels,
|
||||
|
|
@ -722,7 +722,7 @@ export const useConfigs = () => {
|
|||
|
||||
const saveConfig = async (fileName) => {
|
||||
try {
|
||||
await postSaveConfig(fileName);
|
||||
await postDumpConfig(fileName);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,14 +275,14 @@ async def test_videos_downloader_config_api(async_client, test_directory, test_v
|
|||
config = lib.get_videos_downloader_config()
|
||||
assert config.video_resolutions == ['1080p', '720p', '480p', 'maximum']
|
||||
|
||||
request, response = await async_client.get('/api/config/videos_downloader.yaml')
|
||||
request, response = await async_client.get('/api/config?file_name=videos_downloader.yaml')
|
||||
assert response.status == HTTPStatus.OK
|
||||
config = response.json['config']
|
||||
|
||||
# Change the `video_resolutions`
|
||||
config['video_resolutions'] = ['720p', 'maximum']
|
||||
body = dict(config=config)
|
||||
request, response = await async_client.post('/api/config/videos_downloader.yaml', json=body)
|
||||
request, response = await async_client.post('/api/config?file_name=videos_downloader.yaml', json=body)
|
||||
assert response.status == HTTPStatus.NO_CONTENT
|
||||
# Config file was actually changed.
|
||||
config = lib.get_videos_downloader_config()
|
||||
|
|
@ -297,6 +297,6 @@ async def test_videos_downloader_config_api(async_client, test_directory, test_v
|
|||
config = lib.get_videos_downloader_config().dict()
|
||||
config['yt_dlp_options']['file_name_format'] = 'invalid format'
|
||||
body = dict(config=config)
|
||||
request, response = await async_client.post('/api/config/videos_downloader.yaml', json=body)
|
||||
request, response = await async_client.post('/api/config?file_name=videos_downloader.yaml', json=body)
|
||||
assert response.status == HTTPStatus.BAD_REQUEST
|
||||
assert 'Invalid config' in response.json.get('error')
|
||||
|
|
|
|||
72
wrolpi/config_api.py
Normal file
72
wrolpi/config_api.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
from sanic import response, Blueprint
|
||||
from sanic.request import Request
|
||||
from sanic_ext.extensions.openapi import openapi
|
||||
|
||||
from wrolpi import config_schema
|
||||
from wrolpi.api_utils import json_response
|
||||
from wrolpi.common import get_all_configs, get_config_by_file_name
|
||||
from wrolpi.common import logger
|
||||
from wrolpi.errors import InvalidConfig
|
||||
|
||||
config_bp = Blueprint('config', '/api/config')
|
||||
|
||||
|
||||
@config_bp.get('/')
|
||||
@openapi.description('Get the status (imported, valid, etc.) of all configs.')
|
||||
@openapi.parameter('file_name', str, location='query')
|
||||
def get_config(request: Request):
|
||||
if file_name := request.args.get('file_name'):
|
||||
config = get_config_by_file_name(file_name)
|
||||
return json_response(dict(config=config))
|
||||
else:
|
||||
configs = get_all_configs()
|
||||
configs = {k: v.config_status() for k, v in configs.items()}
|
||||
return json_response(dict(configs=configs))
|
||||
|
||||
|
||||
@config_bp.post('/')
|
||||
@openapi.parameter('file_name', str, location='query')
|
||||
@openapi.definition(
|
||||
description='Update the contents of a specific config.',
|
||||
body=config_schema.ConfigUpdateRequest,
|
||||
validate=True,
|
||||
)
|
||||
def post_config_update(request: Request, body: config_schema.ConfigUpdateRequest):
|
||||
file_name, = request.args['file_name']
|
||||
config = get_config_by_file_name(file_name)
|
||||
config.update(body.config)
|
||||
return response.empty()
|
||||
|
||||
|
||||
@config_bp.post('/import')
|
||||
@openapi.definition(
|
||||
description='Import the contents of a specific config.',
|
||||
body=config_schema.ConfigsRequest,
|
||||
validate=True,
|
||||
)
|
||||
def post_config_import(_: Request, body: config_schema.ConfigsRequest):
|
||||
config = get_config_by_file_name(body.file_name)
|
||||
try:
|
||||
config.import_config(send_events=True)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to import config: {body.file_name}', exc_info=e)
|
||||
raise InvalidConfig(f'Failed to import config {body.file_name}')
|
||||
|
||||
return response.empty()
|
||||
|
||||
|
||||
@config_bp.post('/dump')
|
||||
@openapi.definition(
|
||||
description="Dump the contents of a specific config to it's file.",
|
||||
body=config_schema.ConfigsRequest,
|
||||
validate=True,
|
||||
)
|
||||
def post_config_dump(_: Request, body: config_schema.ConfigsRequest):
|
||||
config = get_config_by_file_name(body.file_name)
|
||||
try:
|
||||
config.dump_config(send_events=True, overwrite=body.overwrite)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to dump config: {body.file_name}', exc_info=e)
|
||||
raise InvalidConfig(f'Failed to dump config {body.file_name}')
|
||||
|
||||
return response.empty()
|
||||
22
wrolpi/config_schema.py
Normal file
22
wrolpi/config_schema.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigGetRequest:
|
||||
file_name: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigPostRequest:
|
||||
file_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigsRequest:
|
||||
file_name: str
|
||||
overwrite: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigUpdateRequest:
|
||||
config: dict
|
||||
|
|
@ -23,8 +23,8 @@ from wrolpi.admin import HotspotStatus
|
|||
from wrolpi.api_utils import json_response, api_app
|
||||
from wrolpi.common import logger, get_wrolpi_config, wrol_mode_enabled, get_media_directory, \
|
||||
wrol_mode_check, native_only, disable_wrol_mode, enable_wrol_mode, get_global_statistics, url_strip_host, \
|
||||
set_global_log_level, get_relative_to_media_directory, search_other_estimates, get_all_configs, \
|
||||
get_config_by_file_name
|
||||
set_global_log_level, get_relative_to_media_directory, search_other_estimates
|
||||
from wrolpi.config_api import config_bp
|
||||
from wrolpi.dates import now
|
||||
from wrolpi.db import get_db_session
|
||||
from wrolpi.downloader import download_manager
|
||||
|
|
@ -47,6 +47,7 @@ api_bp = Blueprint('RootAPI', url_prefix='/api')
|
|||
# Blueprints order here defines what order they are displayed in OpenAPI Docs.
|
||||
api_app.blueprint(api_bp)
|
||||
api_app.blueprint(archive_bp)
|
||||
api_app.blueprint(config_bp)
|
||||
api_app.blueprint(files_bp)
|
||||
api_app.blueprint(inventory_bp)
|
||||
api_app.blueprint(map_bp)
|
||||
|
|
@ -670,64 +671,3 @@ async def post_search_other_estimates(_: Request, body: schema.SearchOtherEstima
|
|||
others = await search_other_estimates(body.tag_names)
|
||||
ret = dict(others=others)
|
||||
return json_response(ret)
|
||||
|
||||
|
||||
@api_bp.get('/configs')
|
||||
@openapi.description('Get the status (imported, valid, etc.) of all configs.')
|
||||
def get_configs(_: Request):
|
||||
configs = get_all_configs()
|
||||
configs = {k: v.config_status() for k, v in configs.items()}
|
||||
return json_response(dict(configs=configs))
|
||||
|
||||
|
||||
@api_bp.get('/config/<name:str>')
|
||||
@openapi.description('Get the contents of a specific config.')
|
||||
def get_config(_: Request, name: str):
|
||||
config = get_all_configs()[name]
|
||||
return json_response(dict(config=config))
|
||||
|
||||
|
||||
@api_bp.post('/config/<name:str>')
|
||||
@openapi.definition(
|
||||
description='Save the contents of a specific config.',
|
||||
body=schema.ConfigSaveRequest,
|
||||
validate=True,
|
||||
)
|
||||
def save_config(_: Request, name: str, body: schema.ConfigSaveRequest):
|
||||
config = get_all_configs()[name]
|
||||
config.update(body.config)
|
||||
return response.empty()
|
||||
|
||||
|
||||
@api_bp.post('/configs/import')
|
||||
@openapi.definition(
|
||||
description='Import the contents of a specific config.',
|
||||
body=schema.ConfigsImportRequest,
|
||||
validate=True,
|
||||
)
|
||||
def post_configs_import(_: Request, body: schema.ConfigsImportRequest):
|
||||
config = get_config_by_file_name(body.file_name)
|
||||
try:
|
||||
config.import_config(send_events=True)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to import config: {body.file_name}', exc_info=e)
|
||||
raise InvalidConfig(f'Failed to import config {body.file_name}')
|
||||
|
||||
return response.empty()
|
||||
|
||||
|
||||
@api_bp.post('/configs/save')
|
||||
@openapi.definition(
|
||||
description='Save the contents of a specific config.',
|
||||
body=schema.ConfigsImportRequest,
|
||||
validate=True,
|
||||
)
|
||||
def post_configs_save(_: Request, body: schema.ConfigsImportRequest):
|
||||
config = get_config_by_file_name(body.file_name)
|
||||
try:
|
||||
config.dump_config(send_events=True, overwrite=body.overwrite)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to dump config: {body.file_name}', exc_info=e)
|
||||
raise InvalidConfig(f'Failed to dump config {body.file_name}')
|
||||
|
||||
return response.empty()
|
||||
|
|
|
|||
|
|
@ -371,13 +371,3 @@ class SearchFileEstimateRequest:
|
|||
class SearchOtherEstimateRequest:
|
||||
tag_names: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigsImportRequest:
|
||||
file_name: str
|
||||
overwrite: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigSaveRequest:
|
||||
config: dict
|
||||
|
|
|
|||
68
wrolpi/test/test_config_api.py
Normal file
68
wrolpi/test/test_config_api.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from wrolpi.common import get_wrolpi_config
|
||||
from wrolpi.switches import await_switches
|
||||
from wrolpi.tags import Tag
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_configs_crud(async_client, test_session, test_tags_config, tag_factory, test_wrolpi_config,
|
||||
test_channels_config):
|
||||
"""Configs can be imported or saved on demand."""
|
||||
# Create a tag, save it to config, delete it from the DB.
|
||||
one = await tag_factory()
|
||||
test_session.commit()
|
||||
await await_switches()
|
||||
test_session.delete(one)
|
||||
test_session.commit()
|
||||
assert 'one' in test_tags_config.read_text()
|
||||
assert test_session.query(Tag).count() == 0, 'Tag should have been deleted'
|
||||
|
||||
# Can get list of configs, and if they are valid.
|
||||
request, response = await async_client.get('/api/config')
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
# WROLPiConfig was not saved.
|
||||
assert 'wrolpi.yaml' in response.json['configs']
|
||||
assert response.json['configs']['wrolpi.yaml']['valid'] is False
|
||||
assert response.json['configs']['wrolpi.yaml']['successful_import'] is False
|
||||
# Tags was saved, and is valid.
|
||||
assert 'tags.yaml' in response.json['configs']
|
||||
assert response.json['configs']['tags.yaml']['valid'] is True
|
||||
assert response.json['configs']['wrolpi.yaml']['successful_import'] is False
|
||||
|
||||
# Can import the config.
|
||||
body = dict(file_name='tags.yaml')
|
||||
request, response = await async_client.post('/api/config/import', json=body)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
# Deleted Tag was re-created.
|
||||
assert [i for i, in test_session.query(Tag.name)] == ['one', ]
|
||||
|
||||
# Can trigger a save of a config
|
||||
assert not test_wrolpi_config.exists()
|
||||
body = dict(file_name='wrolpi.yaml')
|
||||
request, response = await async_client.post('/api/config/dump', json=body)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
assert test_wrolpi_config.is_file()
|
||||
assert 'wrol_mode:' in test_wrolpi_config.read_text()
|
||||
|
||||
# Config is now valid because it exists.
|
||||
request, response = await async_client.get('/api/config')
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert 'wrolpi.yaml' in response.json['configs']
|
||||
assert response.json['configs']['wrolpi.yaml']['valid'] is True
|
||||
assert response.json['configs']['tags.yaml']['successful_import'] is True
|
||||
|
||||
# Get and change a config.
|
||||
request, response = await async_client.get('/api/config?file_name=wrolpi.yaml')
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
wrolpi_config = response.json['config']
|
||||
assert wrolpi_config['archive_destination'] == 'archive/%(domain)s'
|
||||
# Change config value.
|
||||
wrolpi_config['archive_destination'] = 'some new value'
|
||||
body = dict(config=wrolpi_config)
|
||||
# Update the config file.
|
||||
request, response = await async_client.post('/api/config?file_name=wrolpi.yaml', json=body)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
assert 'some new value' in get_wrolpi_config().get_file().read_text()
|
||||
|
|
@ -12,7 +12,6 @@ from wrolpi.common import get_wrolpi_config
|
|||
from wrolpi.downloader import Download, get_download_manager_config
|
||||
from wrolpi.errors import ValidationError, SearchEmpty
|
||||
from wrolpi.switches import await_switches
|
||||
from wrolpi.tags import Tag
|
||||
from wrolpi.test.common import skip_circleci, assert_dict_contains
|
||||
|
||||
|
||||
|
|
@ -793,51 +792,3 @@ async def test_search_other_estimates(async_client, test_session, channel_factor
|
|||
request, response = await async_client.post('/api/search_other_estimates', json=body)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json['others']['channel_count'] == 1, 'Only one Channel is tagged.'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_configs(async_client, test_session, test_tags_config, tag_factory, test_wrolpi_config,
|
||||
test_channels_config):
|
||||
"""Tags can be imported or saved on demand."""
|
||||
# Create a tag, save it to config, delete it from the DB.
|
||||
one = await tag_factory()
|
||||
test_session.commit()
|
||||
await await_switches()
|
||||
test_session.delete(one)
|
||||
test_session.commit()
|
||||
assert 'one' in test_tags_config.read_text()
|
||||
assert test_session.query(Tag).count() == 0, 'Tag should have been deleted'
|
||||
|
||||
# Can get list of configs, and if they are valid.
|
||||
request, response = await async_client.get('/api/configs')
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
# WROLPiConfig was not saved.
|
||||
assert 'wrolpi.yaml' in response.json['configs']
|
||||
assert response.json['configs']['wrolpi.yaml']['valid'] is False
|
||||
assert response.json['configs']['wrolpi.yaml']['successful_import'] is False
|
||||
# Tags was saved, and is valid.
|
||||
assert 'tags.yaml' in response.json['configs']
|
||||
assert response.json['configs']['tags.yaml']['valid'] is True
|
||||
assert response.json['configs']['wrolpi.yaml']['successful_import'] is False
|
||||
|
||||
# Can import the config.
|
||||
body = dict(file_name='tags.yaml')
|
||||
request, response = await async_client.post('/api/configs/import', json=body)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
# Deleted Tag was re-created.
|
||||
assert [i for i, in test_session.query(Tag.name)] == ['one', ]
|
||||
|
||||
# Can trigger a save of a config
|
||||
assert not test_wrolpi_config.exists()
|
||||
body = dict(file_name='wrolpi.yaml')
|
||||
request, response = await async_client.post('/api/configs/save', json=body)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
assert test_wrolpi_config.is_file()
|
||||
assert 'wrol_mode:' in test_wrolpi_config.read_text()
|
||||
|
||||
# Config is now valid because it exists.
|
||||
request, response = await async_client.get('/api/configs')
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert 'wrolpi.yaml' in response.json['configs']
|
||||
assert response.json['configs']['wrolpi.yaml']['valid'] is True
|
||||
assert response.json['configs']['tags.yaml']['successful_import'] is True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue