refactor: standardize config_update event for UI
This commit is contained in:
parent
8a10627d6a
commit
ea1fc59e88
13 changed files with 192 additions and 159 deletions
18
API.md
18
API.md
|
|
@ -72,6 +72,7 @@ This document describes the available endpoints and their usage. All endpoints r
|
||||||
- [POST /api/system/pause](#post-apisystempause)
|
- [POST /api/system/pause](#post-apisystempause)
|
||||||
- [POST /api/system/resume](#post-apisystemresume)
|
- [POST /api/system/resume](#post-apisystemresume)
|
||||||
- [POST /api/system/shutdown](#post-apisystemshutdown)
|
- [POST /api/system/shutdown](#post-apisystemshutdown)
|
||||||
|
- [POST /api/system/check-updates](#post-apisystemcheck-updates)
|
||||||
- [GET /api/dev/loop](#get-apidevloop)
|
- [GET /api/dev/loop](#get-apidevloop)
|
||||||
- [GET /api/dev/pip](#get-apidevpip)
|
- [GET /api/dev/pip](#get-apidevpip)
|
||||||
- [GET /api/docs/{file}](#get-apidocsfile)
|
- [GET /api/docs/{file}](#get-apidocsfile)
|
||||||
|
|
@ -102,9 +103,7 @@ This document describes the available endpoints and their usage. All endpoints r
|
||||||
- [`cli_post` (Client → Server)](#cli_post-client--server)
|
- [`cli_post` (Client → Server)](#cli_post-client--server)
|
||||||
- [`cli_output` (Server → Client)](#cli_output-server--client)
|
- [`cli_output` (Server → Client)](#cli_output-server--client)
|
||||||
- [`cli_close` (Server → Client)](#cli_close-server--client)
|
- [`cli_close` (Server → Client)](#cli_close-server--client)
|
||||||
- [Configuration Events](#configuration-events)
|
- [Error Responses](#error-responses)
|
||||||
- [`presets_update` (Server → Client)](#presets_update-server--client)
|
|
||||||
- [`dlfields_update` (Server → Client)](#dlfields_update-server--client)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -2087,19 +2086,6 @@ Emitted when CLI command execution completes.
|
||||||
**Data Fields**:
|
**Data Fields**:
|
||||||
- `exitcode`: Command exit code (0 = success, non-zero = error)
|
- `exitcode`: Command exit code (0 = success, non-zero = error)
|
||||||
|
|
||||||
### Configuration Events
|
|
||||||
|
|
||||||
#### `presets_update` (Server → Client)
|
|
||||||
Emitted when download presets are updated or created.
|
|
||||||
|
|
||||||
**Data**: Array of preset objects
|
|
||||||
|
|
||||||
#### `dlfields_update` (Server → Client)
|
|
||||||
Emitted when download fields configuration is updated.
|
|
||||||
|
|
||||||
**Data**: Array of field objects
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@ from pydantic import ValidationError
|
||||||
|
|
||||||
from app.features.conditions.schemas import Condition, ConditionList, ConditionPatch
|
from app.features.conditions.schemas import Condition, ConditionList, ConditionPatch
|
||||||
from app.features.conditions.service import Conditions
|
from app.features.conditions.service import Conditions
|
||||||
from app.features.core.schemas import Pagination
|
from app.features.core.schemas import CEAction, CEFeature, ConfigEvent, Pagination
|
||||||
from app.features.core.utils import build_pagination, format_validation_errors, normalize_pagination
|
from app.features.core.utils import build_pagination, format_validation_errors, normalize_pagination
|
||||||
from app.library.cache import Cache
|
from app.library.cache import Cache
|
||||||
from app.library.config import Config
|
from app.library.config import Config
|
||||||
from app.library.encoder import Encoder
|
from app.library.encoder import Encoder
|
||||||
|
from app.library.Events import EventBus, Events
|
||||||
from app.library.router import route
|
from app.library.router import route
|
||||||
|
|
||||||
LOG: logging.Logger = logging.getLogger(__name__)
|
LOG: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
@ -136,7 +137,7 @@ async def conditions_test(request: Request, encoder: Encoder, cache: Cache, conf
|
||||||
|
|
||||||
|
|
||||||
@route("POST", "api/conditions/", name="condition_add")
|
@route("POST", "api/conditions/", name="condition_add")
|
||||||
async def conditions_add(request: Request, encoder: Encoder) -> Response:
|
async def conditions_add(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||||
"""
|
"""
|
||||||
Add Condition.
|
Add Condition.
|
||||||
|
|
||||||
|
|
@ -166,15 +167,14 @@ async def conditions_add(request: Request, encoder: Encoder) -> Response:
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = _serialize(await Conditions.get_instance().save(item=item.model_dump()))
|
saved = _serialize(await Conditions.get_instance().save(item=item.model_dump()))
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return web.json_response({"error": str(exc)}, status=web.HTTPBadRequest.status_code)
|
return web.json_response({"error": str(exc)}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
return web.json_response(
|
notify.emit(
|
||||||
data=data,
|
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.CREATE, data=saved)
|
||||||
status=web.HTTPOk.status_code,
|
|
||||||
dumps=encoder.encode,
|
|
||||||
)
|
)
|
||||||
|
return web.json_response(data=saved, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
|
|
||||||
|
|
||||||
@route("GET", "api/conditions/{id}", name="condition_get")
|
@route("GET", "api/conditions/{id}", name="condition_get")
|
||||||
|
|
@ -200,13 +200,14 @@ async def conditions_get(request: Request, encoder: Encoder) -> Response:
|
||||||
|
|
||||||
|
|
||||||
@route("DELETE", "api/conditions/{id}", name="condition_delete")
|
@route("DELETE", "api/conditions/{id}", name="condition_delete")
|
||||||
async def conditions_delete(request: Request, encoder: Encoder) -> Response:
|
async def conditions_delete(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||||
"""
|
"""
|
||||||
Delete Condition.
|
Delete Condition.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (Request): The request object.
|
request (Request): The request object.
|
||||||
encoder (Encoder): The encoder instance.
|
encoder (Encoder): The encoder instance.
|
||||||
|
notify (EventBus): The event bus instance.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: The response object.
|
Response: The response object.
|
||||||
|
|
@ -216,23 +217,24 @@ async def conditions_delete(request: Request, encoder: Encoder) -> Response:
|
||||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return web.json_response(
|
deleted = _serialize(await Conditions.get_instance()._repo.delete(id))
|
||||||
data=_serialize(await Conditions.get_instance()._repo.delete(id)),
|
notify.emit(
|
||||||
status=web.HTTPOk.status_code,
|
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.DELETE, data=deleted)
|
||||||
dumps=encoder.encode,
|
|
||||||
)
|
)
|
||||||
|
return web.json_response(data=deleted, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
return web.json_response({"error": str(exc)}, status=web.HTTPNotFound.status_code)
|
return web.json_response({"error": str(exc)}, status=web.HTTPNotFound.status_code)
|
||||||
|
|
||||||
|
|
||||||
@route("PATCH", "api/conditions/{id}", name="condition_patch")
|
@route("PATCH", "api/conditions/{id}", name="condition_patch")
|
||||||
async def conditions_patch(request: Request, encoder: Encoder) -> Response:
|
async def conditions_patch(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||||
"""
|
"""
|
||||||
Patch Condition.
|
Patch Condition.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (Request): The request object.
|
request (Request): The request object.
|
||||||
encoder (Encoder): The encoder instance.
|
encoder (Encoder): The encoder instance.
|
||||||
|
notify (EventBus): The event bus instance.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: The response object.
|
Response: The response object.
|
||||||
|
|
@ -268,23 +270,22 @@ async def conditions_patch(request: Request, encoder: Encoder) -> Response:
|
||||||
status=web.HTTPConflict.status_code,
|
status=web.HTTPConflict.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
dct = validated.model_dump(exclude_unset=True)
|
updated = _serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||||
|
notify.emit(
|
||||||
return web.json_response(
|
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.UPDATE, data=updated)
|
||||||
data=_serialize(await service._repo.update(model.id, dct)),
|
|
||||||
status=web.HTTPOk.status_code,
|
|
||||||
dumps=encoder.encode,
|
|
||||||
)
|
)
|
||||||
|
return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
|
|
||||||
|
|
||||||
@route("PUT", "api/conditions/{id}", name="condition_update")
|
@route("PUT", "api/conditions/{id}", name="condition_update")
|
||||||
async def conditions_update(request: Request, encoder: Encoder) -> Response:
|
async def conditions_update(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||||
"""
|
"""
|
||||||
Update Condition.
|
Update Condition.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (Request): The request object.
|
request (Request): The request object.
|
||||||
encoder (Encoder): The encoder instance.
|
encoder (Encoder): The encoder instance.
|
||||||
|
notify (EventBus): The event bus instance.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: The response object.
|
Response: The response object.
|
||||||
|
|
@ -320,8 +321,8 @@ async def conditions_update(request: Request, encoder: Encoder) -> Response:
|
||||||
status=web.HTTPConflict.status_code,
|
status=web.HTTPConflict.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.json_response(
|
updated = _serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||||
data=_serialize(await service._repo.update(model.id, validated.model_dump(exclude_unset=True))),
|
notify.emit(
|
||||||
status=web.HTTPOk.status_code,
|
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.CONDITIONS, action=CEAction.UPDATE, data=updated)
|
||||||
dumps=encoder.encode,
|
|
||||||
)
|
)
|
||||||
|
return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class Pagination(BaseModel):
|
class Pagination(BaseModel):
|
||||||
|
|
@ -10,3 +13,29 @@ class Pagination(BaseModel):
|
||||||
total_pages: int
|
total_pages: int
|
||||||
has_next: bool
|
has_next: bool
|
||||||
has_prev: bool
|
has_prev: bool
|
||||||
|
|
||||||
|
|
||||||
|
class CEFeature(str, Enum):
|
||||||
|
PRESETS = "presets"
|
||||||
|
DL_FIELDS = "dl_fields"
|
||||||
|
CONDITIONS = "conditions"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class CEAction(str, Enum):
|
||||||
|
CREATE = "create"
|
||||||
|
UPDATE = "update"
|
||||||
|
DELETE = "delete"
|
||||||
|
REPLACE = "replace"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigEvent(BaseModel):
|
||||||
|
feature: CEFeature
|
||||||
|
action: CEAction
|
||||||
|
data: dict[str, Any] | list[dict[str, Any]]
|
||||||
|
extras: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from aiohttp import web
|
||||||
from aiohttp.web import Request, Response
|
from aiohttp.web import Request, Response
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from app.features.core.schemas import Pagination
|
from app.features.core.schemas import CEAction, CEFeature, ConfigEvent, Pagination
|
||||||
from app.features.core.utils import build_pagination, format_validation_errors, normalize_pagination
|
from app.features.core.utils import build_pagination, format_validation_errors, normalize_pagination
|
||||||
from app.features.dl_fields.schemas import DLField, DLFieldList, DLFieldPatch
|
from app.features.dl_fields.schemas import DLField, DLFieldList, DLFieldPatch
|
||||||
from app.features.dl_fields.service import DLFields
|
from app.features.dl_fields.service import DLFields
|
||||||
|
|
@ -62,7 +62,7 @@ async def dl_fields_add(request: Request, encoder: Encoder, notify: EventBus) ->
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return web.json_response({"error": str(exc)}, status=web.HTTPBadRequest.status_code)
|
return web.json_response({"error": str(exc)}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
notify.emit(Events.DLFIELDS_UPDATE, data=[saved])
|
notify.emit(Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.CREATE, data=saved))
|
||||||
|
|
||||||
return web.json_response(data=saved, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
return web.json_response(data=saved, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
|
|
||||||
|
|
@ -79,13 +79,17 @@ async def dl_fields_get(request: Request, encoder: Encoder) -> Response:
|
||||||
|
|
||||||
|
|
||||||
@route("DELETE", "api/dl_fields/{id}", "dl_fields_delete")
|
@route("DELETE", "api/dl_fields/{id}", "dl_fields_delete")
|
||||||
async def dl_fields_delete(request: Request, encoder: Encoder) -> Response:
|
async def dl_fields_delete(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||||
if not (identifier := request.match_info.get("id")):
|
if not (identifier := request.match_info.get("id")):
|
||||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
deleted = _serialize(await DLFields.get_instance()._repo.delete(identifier))
|
||||||
|
notify.emit(
|
||||||
|
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.DELETE, data=deleted)
|
||||||
|
)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
data=_serialize(await DLFields.get_instance()._repo.delete(identifier)),
|
data=deleted,
|
||||||
status=web.HTTPOk.status_code,
|
status=web.HTTPOk.status_code,
|
||||||
dumps=encoder.encode,
|
dumps=encoder.encode,
|
||||||
)
|
)
|
||||||
|
|
@ -94,7 +98,7 @@ async def dl_fields_delete(request: Request, encoder: Encoder) -> Response:
|
||||||
|
|
||||||
|
|
||||||
@route("PATCH", "api/dl_fields/{id}", "dl_fields_patch")
|
@route("PATCH", "api/dl_fields/{id}", "dl_fields_patch")
|
||||||
async def dl_fields_patch(request: Request, encoder: Encoder) -> Response:
|
async def dl_fields_patch(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||||
if not (identifier := request.match_info.get("id")):
|
if not (identifier := request.match_info.get("id")):
|
||||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
|
@ -123,17 +127,19 @@ async def dl_fields_patch(request: Request, encoder: Encoder) -> Response:
|
||||||
status=web.HTTPConflict.status_code,
|
status=web.HTTPConflict.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
dct = validated.model_dump(exclude_unset=True)
|
updated = _serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||||
|
notify.emit(
|
||||||
|
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.UPDATE, data=updated)
|
||||||
|
)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
data=_serialize(await DLFields.get_instance()._repo.update(model.id, dct)),
|
data=updated,
|
||||||
status=web.HTTPOk.status_code,
|
status=web.HTTPOk.status_code,
|
||||||
dumps=encoder.encode,
|
dumps=encoder.encode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@route("PUT", "api/dl_fields/{id}", "dl_fields_update")
|
@route("PUT", "api/dl_fields/{id}", "dl_fields_update")
|
||||||
async def dl_fields_update(request: Request, encoder: Encoder) -> Response:
|
async def dl_fields_update(request: Request, encoder: Encoder, notify: EventBus) -> Response:
|
||||||
if not (identifier := request.match_info.get("id")):
|
if not (identifier := request.match_info.get("id")):
|
||||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
|
@ -162,8 +168,9 @@ async def dl_fields_update(request: Request, encoder: Encoder) -> Response:
|
||||||
status=web.HTTPConflict.status_code,
|
status=web.HTTPConflict.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.json_response(
|
updated = _serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||||
data=_serialize(await DLFields.get_instance()._repo.update(model.id, validated.model_dump(exclude_unset=True))),
|
notify.emit(
|
||||||
status=web.HTTPOk.status_code,
|
Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.DL_FIELDS, action=CEAction.UPDATE, data=updated)
|
||||||
dumps=encoder.encode,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return web.json_response(data=updated, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ class Events:
|
||||||
CONNECTED: str = "connected"
|
CONNECTED: str = "connected"
|
||||||
|
|
||||||
CONFIGURATION: str = "configuration"
|
CONFIGURATION: str = "configuration"
|
||||||
|
CONFIG_UPDATE: str = "config_update"
|
||||||
ACTIVE_QUEUE: str = "active_queue"
|
ACTIVE_QUEUE: str = "active_queue"
|
||||||
|
|
||||||
LOG_INFO: str = "log_info"
|
LOG_INFO: str = "log_info"
|
||||||
|
|
@ -58,17 +59,8 @@ class Events:
|
||||||
TASK_FINISHED: str = "task_finished"
|
TASK_FINISHED: str = "task_finished"
|
||||||
TASK_ERROR: str = "task_error"
|
TASK_ERROR: str = "task_error"
|
||||||
|
|
||||||
PRESETS_ADD: str = "presets_add"
|
|
||||||
PRESETS_UPDATE: str = "presets_update"
|
|
||||||
|
|
||||||
DLFIELDS_ADD: str = "dlfields_add"
|
|
||||||
DLFIELDS_UPDATE: str = "dlfields_update"
|
|
||||||
|
|
||||||
SCHEDULE_ADD: str = "schedule_add"
|
SCHEDULE_ADD: str = "schedule_add"
|
||||||
|
|
||||||
CONDITIONS_ADD: str = "conditions_add"
|
|
||||||
CONDITIONS_UPDATE: str = "conditions_update"
|
|
||||||
|
|
||||||
SUBSCRIBED: str = "subscribed"
|
SUBSCRIBED: str = "subscribed"
|
||||||
UNSUBSCRIBED: str = "unsubscribed"
|
UNSUBSCRIBED: str = "unsubscribed"
|
||||||
|
|
||||||
|
|
@ -94,6 +86,7 @@ class Events:
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
Events.CONFIGURATION,
|
Events.CONFIGURATION,
|
||||||
|
Events.CONFIG_UPDATE,
|
||||||
Events.CONNECTED,
|
Events.CONNECTED,
|
||||||
Events.ACTIVE_QUEUE,
|
Events.ACTIVE_QUEUE,
|
||||||
Events.LOG_INFO,
|
Events.LOG_INFO,
|
||||||
|
|
@ -110,8 +103,6 @@ class Events:
|
||||||
Events.RESUMED,
|
Events.RESUMED,
|
||||||
Events.CLI_CLOSE,
|
Events.CLI_CLOSE,
|
||||||
Events.CLI_OUTPUT,
|
Events.CLI_OUTPUT,
|
||||||
Events.PRESETS_UPDATE,
|
|
||||||
Events.DLFIELDS_UPDATE,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def only_debug() -> list:
|
def only_debug() -> list:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from typing import Any
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .Events import EventBus, Events
|
|
||||||
from .Singleton import Singleton
|
from .Singleton import Singleton
|
||||||
from .Utils import arg_converter, create_cookies_file, init_class
|
from .Utils import arg_converter, create_cookies_file, init_class
|
||||||
|
|
||||||
|
|
@ -214,12 +213,6 @@ class Presets(metaclass=Singleton):
|
||||||
LOG.error(f"Default preset '{self._config.default_preset}' not found, using 'default' preset.")
|
LOG.error(f"Default preset '{self._config.default_preset}' not found, using 'default' preset.")
|
||||||
self._config.default_preset = "default"
|
self._config.default_preset = "default"
|
||||||
|
|
||||||
async def event_handler(_, __):
|
|
||||||
msg = "Not implemented"
|
|
||||||
raise Exception(msg)
|
|
||||||
|
|
||||||
EventBus.get_instance().subscribe(Events.PRESETS_ADD, event_handler, f"{__class__.__name__}.add")
|
|
||||||
|
|
||||||
def get_all(self) -> list[Preset]:
|
def get_all(self) -> list[Preset]:
|
||||||
"""Return the items."""
|
"""Return the items."""
|
||||||
return sorted(self._default + self._items, key=lambda x: x.priority, reverse=True)
|
return sorted(self._default + self._items, key=lambda x: x.priority, reverse=True)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import uuid
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.web import Request, Response
|
from aiohttp.web import Request, Response
|
||||||
|
|
||||||
|
from app.features.core.schemas import CEAction, CEFeature, ConfigEvent
|
||||||
from app.library.encoder import Encoder
|
from app.library.encoder import Encoder
|
||||||
from app.library.Events import EventBus, Events
|
from app.library.Events import EventBus, Events
|
||||||
from app.library.Presets import Preset, Presets
|
from app.library.Presets import Preset, Presets
|
||||||
|
|
@ -96,5 +97,12 @@ async def presets_add(request: Request, encoder: Encoder, notify: EventBus) -> R
|
||||||
status=web.HTTPInternalServerError.status_code,
|
status=web.HTTPInternalServerError.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
notify.emit(Events.PRESETS_UPDATE, data=presets)
|
notify.emit(
|
||||||
|
Events.CONFIG_UPDATE,
|
||||||
|
data=ConfigEvent(
|
||||||
|
feature=CEFeature.PRESETS,
|
||||||
|
action=CEAction.REPLACE,
|
||||||
|
data=[preset.serialize() for preset in presets],
|
||||||
|
),
|
||||||
|
)
|
||||||
return web.json_response(data=presets, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
return web.json_response(data=presets, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class TestEvents:
|
||||||
|
|
||||||
# Connection events
|
# Connection events
|
||||||
assert Events.CONNECTED == "connected"
|
assert Events.CONNECTED == "connected"
|
||||||
|
assert Events.CONFIG_UPDATE == "config_update"
|
||||||
|
|
||||||
# Log events
|
# Log events
|
||||||
assert Events.LOG_INFO == "log_info"
|
assert Events.LOG_INFO == "log_info"
|
||||||
|
|
@ -48,6 +49,7 @@ class TestEvents:
|
||||||
"started",
|
"started",
|
||||||
"shutdown",
|
"shutdown",
|
||||||
"connected",
|
"connected",
|
||||||
|
"config_update",
|
||||||
"log_info",
|
"log_info",
|
||||||
"log_warning",
|
"log_warning",
|
||||||
"log_error",
|
"log_error",
|
||||||
|
|
@ -85,6 +87,7 @@ class TestEvents:
|
||||||
# Check some expected frontend events
|
# Check some expected frontend events
|
||||||
expected_frontend = [
|
expected_frontend = [
|
||||||
Events.CONNECTED,
|
Events.CONNECTED,
|
||||||
|
Events.CONFIG_UPDATE,
|
||||||
Events.LOG_INFO,
|
Events.LOG_INFO,
|
||||||
Events.LOG_WARNING,
|
Events.LOG_WARNING,
|
||||||
Events.LOG_ERROR,
|
Events.LOG_ERROR,
|
||||||
|
|
|
||||||
|
|
@ -103,11 +103,9 @@ class TestPresets:
|
||||||
Presets._reset_singleton()
|
Presets._reset_singleton()
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_singleton(self, mock_config):
|
||||||
def test_presets_singleton(self, mock_eventbus, mock_config):
|
|
||||||
"""Test that Presets follows singleton pattern."""
|
"""Test that Presets follows singleton pattern."""
|
||||||
mock_config.get_instance.return_value = Mock(config_path="/tmp", default_preset="default")
|
mock_config.get_instance.return_value = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
instance1 = Presets.get_instance()
|
instance1 = Presets.get_instance()
|
||||||
instance2 = Presets.get_instance()
|
instance2 = Presets.get_instance()
|
||||||
|
|
@ -116,13 +114,10 @@ class TestPresets:
|
||||||
assert isinstance(instance1, Presets)
|
assert isinstance(instance1, Presets)
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_initialization(self, mock_config):
|
||||||
def test_presets_initialization(self, mock_eventbus, mock_config):
|
|
||||||
"""Test Presets initialization with default presets."""
|
"""Test Presets initialization with default presets."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus_instance = Mock()
|
|
||||||
mock_eventbus.get_instance.return_value = mock_eventbus_instance
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -138,12 +133,10 @@ class TestPresets:
|
||||||
assert preset.default is True
|
assert preset.default is True
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_load_empty_file(self, mock_config):
|
||||||
def test_presets_load_empty_file(self, mock_eventbus, mock_config):
|
|
||||||
"""Test loading presets from empty or non-existent file."""
|
"""Test loading presets from empty or non-existent file."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -157,13 +150,11 @@ class TestPresets:
|
||||||
assert len(presets._default) > 0 # Should still have default presets
|
assert len(presets._default) > 0 # Should still have default presets
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
|
||||||
@patch("app.library.Presets.arg_converter")
|
@patch("app.library.Presets.arg_converter")
|
||||||
def test_presets_load_valid_file(self, mock_arg_converter, mock_eventbus, mock_config):
|
def test_presets_load_valid_file(self, mock_arg_converter, mock_config):
|
||||||
"""Test loading presets from valid JSON file."""
|
"""Test loading presets from valid JSON file."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
||||||
|
|
||||||
test_presets = [
|
test_presets = [
|
||||||
|
|
@ -184,13 +175,11 @@ class TestPresets:
|
||||||
assert presets._items[0].name == "test_preset_1"
|
assert presets._items[0].name == "test_preset_1"
|
||||||
assert presets._items[1].name == "test_preset_2"
|
assert presets._items[1].name == "test_preset_2"
|
||||||
|
|
||||||
@patch("app.library.Presets.EventBus")
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
def test_presets_load_invalid_json(self, mock_eventbus, mock_config):
|
def test_presets_load_invalid_json(self, mock_config):
|
||||||
"""Test loading presets from invalid JSON file."""
|
"""Test loading presets from invalid JSON file."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -204,13 +193,11 @@ class TestPresets:
|
||||||
assert len(presets._items) == 0
|
assert len(presets._items) == 0
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
|
||||||
@patch("app.library.Presets.arg_converter")
|
@patch("app.library.Presets.arg_converter")
|
||||||
def test_presets_load_with_format_migration(self, mock_arg_converter, mock_eventbus, mock_config):
|
def test_presets_load_with_format_migration(self, mock_arg_converter, mock_config):
|
||||||
"""Test loading presets with old format that needs migration."""
|
"""Test loading presets with old format that needs migration."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
||||||
|
|
||||||
# Old format preset with 'format' field instead of 'cli'
|
# Old format preset with 'format' field instead of 'cli'
|
||||||
|
|
@ -233,12 +220,10 @@ class TestPresets:
|
||||||
assert loaded_preset.id is not None
|
assert loaded_preset.id is not None
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_validate_valid_preset(self, mock_config):
|
||||||
def test_presets_validate_valid_preset(self, mock_eventbus, mock_config):
|
|
||||||
"""Test validating a valid preset."""
|
"""Test validating a valid preset."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -250,12 +235,10 @@ class TestPresets:
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_validate_invalid_preset(self, mock_config):
|
||||||
def test_presets_validate_invalid_preset(self, mock_eventbus, mock_config):
|
|
||||||
"""Test validating invalid presets."""
|
"""Test validating invalid presets."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -274,13 +257,11 @@ class TestPresets:
|
||||||
presets.validate("invalid_type")
|
presets.validate("invalid_type")
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
|
||||||
@patch("app.library.Presets.arg_converter")
|
@patch("app.library.Presets.arg_converter")
|
||||||
def test_presets_save(self, mock_arg_converter, mock_eventbus, mock_config):
|
def test_presets_save(self, mock_arg_converter, mock_config):
|
||||||
"""Test saving presets to file."""
|
"""Test saving presets to file."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
||||||
|
|
||||||
test_presets = [
|
test_presets = [
|
||||||
|
|
@ -303,12 +284,10 @@ class TestPresets:
|
||||||
assert saved_data[0]["name"] == "test1"
|
assert saved_data[0]["name"] == "test1"
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_get_all(self, mock_config):
|
||||||
def test_presets_get_all(self, mock_eventbus, mock_config):
|
|
||||||
"""Test getting all presets (default + custom)."""
|
"""Test getting all presets (default + custom)."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -325,12 +304,10 @@ class TestPresets:
|
||||||
assert any(p.default is True for p in all_presets)
|
assert any(p.default is True for p in all_presets)
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_get_by_id(self, mock_config):
|
||||||
def test_presets_get_by_id(self, mock_eventbus, mock_config):
|
|
||||||
"""Test getting preset by ID."""
|
"""Test getting preset by ID."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -347,12 +324,10 @@ class TestPresets:
|
||||||
assert found_preset.name == "test_preset"
|
assert found_preset.name == "test_preset"
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_get_by_name(self, mock_config):
|
||||||
def test_presets_get_by_name(self, mock_eventbus, mock_config):
|
|
||||||
"""Test getting preset by name."""
|
"""Test getting preset by name."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -368,12 +343,10 @@ class TestPresets:
|
||||||
assert found_preset.cli == "--test"
|
assert found_preset.cli == "--test"
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_get_nonexistent(self, mock_config):
|
||||||
def test_presets_get_nonexistent(self, mock_eventbus, mock_config):
|
|
||||||
"""Test getting non-existent preset returns None."""
|
"""Test getting non-existent preset returns None."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -384,12 +357,10 @@ class TestPresets:
|
||||||
assert found_preset is None
|
assert found_preset is None
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_get_empty_parameter(self, mock_config):
|
||||||
def test_presets_get_empty_parameter(self, mock_eventbus, mock_config):
|
|
||||||
"""Test getting preset with empty string returns None."""
|
"""Test getting preset with empty string returns None."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -399,12 +370,10 @@ class TestPresets:
|
||||||
assert presets.get(None) is None
|
assert presets.get(None) is None
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_has_method(self, mock_config):
|
||||||
def test_presets_has_method(self, mock_eventbus, mock_config):
|
|
||||||
"""Test has method for checking preset existence."""
|
"""Test has method for checking preset existence."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -418,12 +387,10 @@ class TestPresets:
|
||||||
assert presets.has("nonexistent") is False
|
assert presets.has("nonexistent") is False
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_clear(self, mock_config):
|
||||||
def test_presets_clear(self, mock_eventbus, mock_config):
|
|
||||||
"""Test clearing all custom presets."""
|
"""Test clearing all custom presets."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -441,12 +408,10 @@ class TestPresets:
|
||||||
assert len(presets._items) == 0
|
assert len(presets._items) == 0
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_file_permissions(self, mock_config):
|
||||||
def test_presets_file_permissions(self, mock_eventbus, mock_config):
|
|
||||||
"""Test that presets file gets correct permissions."""
|
"""Test that presets file gets correct permissions."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -483,12 +448,10 @@ class TestPresets:
|
||||||
assert isinstance(preset, Preset)
|
assert isinstance(preset, Preset)
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_attach_method(self, mock_config):
|
||||||
def test_presets_attach_method(self, mock_eventbus, mock_config):
|
|
||||||
"""Test the attach method for aiohttp integration."""
|
"""Test the attach method for aiohttp integration."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
mock_app = Mock()
|
mock_app = Mock()
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
|
@ -501,12 +464,10 @@ class TestPresets:
|
||||||
mock_get.assert_called_once_with("default")
|
mock_get.assert_called_once_with("default")
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_presets_attach_default_preset_not_found(self, mock_config):
|
||||||
def test_presets_attach_default_preset_not_found(self, mock_eventbus, mock_config):
|
|
||||||
"""Test attach method when default preset is not found."""
|
"""Test attach method when default preset is not found."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="nonexistent")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="nonexistent")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
mock_app = Mock()
|
mock_app = Mock()
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
|
@ -520,12 +481,10 @@ class TestPresets:
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
async def test_presets_on_shutdown(self, mock_config):
|
||||||
async def test_presets_on_shutdown(self, mock_eventbus, mock_config):
|
|
||||||
"""Test the on_shutdown method."""
|
"""Test the on_shutdown method."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
mock_app = Mock()
|
mock_app = Mock()
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
|
@ -536,14 +495,12 @@ class TestPresets:
|
||||||
await presets.on_shutdown(mock_app)
|
await presets.on_shutdown(mock_app)
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
|
||||||
@patch("app.library.Presets.LOG")
|
@patch("app.library.Presets.LOG")
|
||||||
@patch("app.library.Presets.arg_converter")
|
@patch("app.library.Presets.arg_converter")
|
||||||
def test_presets_load_invalid_preset_in_file(self, mock_arg_converter, mock_log, mock_eventbus, mock_config):
|
def test_presets_load_invalid_preset_in_file(self, mock_arg_converter, mock_log, mock_config):
|
||||||
"""Test loading file with some invalid presets."""
|
"""Test loading file with some invalid presets."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
mock_arg_converter.return_value = [] # Mock successful CLI parsing
|
||||||
|
|
||||||
# Mix of valid and invalid presets
|
# Mix of valid and invalid presets
|
||||||
|
|
@ -570,13 +527,11 @@ class TestPresets:
|
||||||
mock_log.error.assert_called()
|
mock_log.error.assert_called()
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
|
||||||
@patch("app.library.Presets.LOG")
|
@patch("app.library.Presets.LOG")
|
||||||
def test_presets_save_with_invalid_preset(self, mock_log, mock_eventbus, mock_config):
|
def test_presets_save_with_invalid_preset(self, mock_log, mock_config):
|
||||||
"""Test saving presets with some invalid ones."""
|
"""Test saving presets with some invalid ones."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
# Create preset with invalid CLI that will fail validation
|
# Create preset with invalid CLI that will fail validation
|
||||||
valid_preset = Preset(name="valid", cli="--valid-option")
|
valid_preset = Preset(name="valid", cli="--valid-option")
|
||||||
|
|
@ -596,12 +551,10 @@ class TestPresets:
|
||||||
mock_log.error.assert_called()
|
mock_log.error.assert_called()
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_preset_priority_validation_integer(self, mock_config):
|
||||||
def test_preset_priority_validation_integer(self, mock_eventbus, mock_config):
|
|
||||||
"""Test that priority must be an integer."""
|
"""Test that priority must be an integer."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -614,12 +567,10 @@ class TestPresets:
|
||||||
presets.validate(invalid_preset)
|
presets.validate(invalid_preset)
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_preset_priority_validation_negative(self, mock_config):
|
||||||
def test_preset_priority_validation_negative(self, mock_eventbus, mock_config):
|
|
||||||
"""Test that priority must be >= 0."""
|
"""Test that priority must be >= 0."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -632,12 +583,10 @@ class TestPresets:
|
||||||
presets.validate(invalid_preset)
|
presets.validate(invalid_preset)
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_preset_priority_sorting(self, mock_config):
|
||||||
def test_preset_priority_sorting(self, mock_eventbus, mock_config):
|
|
||||||
"""Test that presets are sorted by priority in descending order."""
|
"""Test that presets are sorted by priority in descending order."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
@ -661,19 +610,16 @@ class TestPresets:
|
||||||
assert non_default[2].priority == 1
|
assert non_default[2].priority == 1
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_preset_priority_default_zero(self, mock_config):
|
||||||
def test_preset_priority_default_zero(self, mock_eventbus, mock_config):
|
|
||||||
"""Test that priority defaults to 0 if not specified."""
|
"""Test that priority defaults to 0 if not specified."""
|
||||||
preset = Preset(name="test")
|
preset = Preset(name="test")
|
||||||
assert preset.priority == 0
|
assert preset.priority == 0
|
||||||
|
|
||||||
@patch("app.library.Presets.Config")
|
@patch("app.library.Presets.Config")
|
||||||
@patch("app.library.Presets.EventBus")
|
def test_preset_priority_migration(self, mock_config):
|
||||||
def test_preset_priority_migration(self, mock_eventbus, mock_config):
|
|
||||||
"""Test that old presets without priority get it added on load."""
|
"""Test that old presets without priority get it added on load."""
|
||||||
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
mock_config_instance = Mock(config_path="/tmp", default_preset="default")
|
||||||
mock_config.get_instance.return_value = mock_config_instance
|
mock_config.get_instance.return_value = mock_config_instance
|
||||||
mock_eventbus.get_instance.return_value = Mock()
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
presets_file = Path(temp_dir) / "presets.json"
|
presets_file = Path(temp_dir) / "presets.json"
|
||||||
|
|
|
||||||
|
|
@ -452,7 +452,7 @@
|
||||||
<li><code>source_name:task_name</code> - items added by the specified task.</li>
|
<li><code>source_name:task_name</code> - items added by the specified task.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Message>
|
</Message>
|
||||||
<Message v-else-if="socket.isConnected" class="is-primary" title="No items" icon="fas fa-exclamation-triangle"
|
<Message v-else class="is-primary" title="No items" icon="fas fa-exclamation-triangle"
|
||||||
:new-style="true">
|
:new-style="true">
|
||||||
<p>Download history is empty.</p>
|
<p>Download history is empty.</p>
|
||||||
</Message>
|
</Message>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import type { ConfigState } from '~/types/config';
|
import type { ConfigState } from '~/types/config';
|
||||||
|
import type { DLField } from '~/types/dl_fields';
|
||||||
|
import type { Preset } from '~/types/presets';
|
||||||
|
import type { ConfigFeature, ConfigUpdateAction } from '~/types/sockets';
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
const state = reactive<ConfigState>({
|
const state = reactive<ConfigState>({
|
||||||
|
|
@ -65,6 +68,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||||
}
|
}
|
||||||
return (state as any)[key] ?? defaultValue
|
return (state as any)[key] ?? defaultValue
|
||||||
}
|
}
|
||||||
|
const isLoaded = () => state.is_loaded
|
||||||
|
|
||||||
const update = add
|
const update = add
|
||||||
|
|
||||||
|
|
@ -84,16 +88,62 @@ export const useConfigStore = defineStore('config', () => {
|
||||||
state.is_loaded = true
|
state.is_loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoaded = () => state.is_loaded
|
const patch = (feature: ConfigFeature, action: ConfigUpdateAction, data: unknown): void => {
|
||||||
|
const supportedFeatures: ConfigFeature[] = ['dl_fields', 'presets']
|
||||||
|
|
||||||
|
if (!supportedFeatures.includes(feature)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('presets' === feature) {
|
||||||
|
if ('replace' === action) {
|
||||||
|
state.presets = data as Array<Preset>
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('dl_fields' === feature) {
|
||||||
|
const item = data as DLField
|
||||||
|
const current = get(feature, []) as Array<DLField>
|
||||||
|
|
||||||
|
if ('create' === action) {
|
||||||
|
current.push(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('delete' === action) {
|
||||||
|
const index = current.findIndex(i => i.id === item.id)
|
||||||
|
if (-1 !== index) {
|
||||||
|
current.splice(index, 1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('update' === action) {
|
||||||
|
const target = current.find(i => i.id === item.id)
|
||||||
|
if (target) {
|
||||||
|
Object.assign(target, item)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ('replace' === action) {
|
||||||
|
state.dl_fields = data as Array<DLField>
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...toRefs(state), add, get, update, getAll, setAll, isLoaded,
|
...toRefs(state), add, get, update, getAll, setAll, isLoaded, patch
|
||||||
} as { [K in keyof ConfigState]: Ref<ConfigState[K]> } & {
|
} as { [K in keyof ConfigState]: Ref<ConfigState[K]> } & {
|
||||||
add: typeof add
|
add: typeof add
|
||||||
get: typeof get
|
get: typeof get
|
||||||
update: typeof update
|
update: typeof update
|
||||||
getAll: typeof getAll
|
getAll: typeof getAll
|
||||||
setAll: typeof setAll
|
setAll: typeof setAll
|
||||||
|
patch: typeof patch
|
||||||
isLoaded: typeof isLoaded
|
isLoaded: typeof isLoaded
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { ref, readonly } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { ConfigState } from "~/types/config";
|
import type { ConfigState } from "~/types/config";
|
||||||
import type { StoreItem } from "~/types/store";
|
import type { StoreItem } from "~/types/store";
|
||||||
|
import type { ConfigUpdatePayload } from "~/types/sockets";
|
||||||
|
|
||||||
export type connectionStatus = 'connected' | 'disconnected' | 'connecting';
|
export type connectionStatus = 'connected' | 'disconnected' | 'connecting';
|
||||||
|
|
||||||
|
|
@ -277,8 +278,13 @@ export const useSocketStore = defineStore('socket', () => {
|
||||||
toast.warning('Download queue paused.', { timeout: 10000 });
|
toast.warning('Download queue paused.', { timeout: 10000 });
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
on('presets_update', (data: string) => config.update('presets', JSON.parse(data).data || []));
|
on('config_update', (stream: string) => {
|
||||||
on('dlfields_update', (data: string) => config.update('dl_fields', JSON.parse(data).data || []));
|
const json = JSON.parse(stream) as { data: ConfigUpdatePayload }
|
||||||
|
if (!json?.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.patch(json.data.feature, json.data.action, json.data.data)
|
||||||
|
})
|
||||||
|
|
||||||
setupVisibilityListener();
|
setupVisibilityListener();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
ui/app/types/sockets.d.ts
vendored
13
ui/app/types/sockets.d.ts
vendored
|
|
@ -6,3 +6,16 @@ export type Event = {
|
||||||
message: string | null
|
message: string | null
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigUpdateAction = 'create' | 'update' | 'delete' | 'replace'
|
||||||
|
|
||||||
|
type ConfigFeature = 'presets' | 'dl_fields' | 'conditions'
|
||||||
|
|
||||||
|
type ConfigUpdatePayload<T = unknown> = {
|
||||||
|
feature: ConfigFeature
|
||||||
|
action: ConfigUpdateAction
|
||||||
|
data: T | Array<T>
|
||||||
|
meta?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ConfigUpdateAction, ConfigFeature, ConfigUpdatePayload }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue