mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
API JSON: encode paths without a lookup per path
CustomJSONEncoder looked up the media directory and called Path.relative_to for every path it encoded. A response with hundreds of paths (an Archive's page with 51 history entries holds 416) spent most of its encoding time there: 18.8 ms of 19 ms per response, measured on a Mac. The encoder now looks up the media directory once per response and strips it as a string prefix: 1.5 ms for the same response. Paths outside the media directory, relative paths, and siblings that share the media directory's name prefix are still returned unchanged; the media directory itself and "." are still "". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dab333124e
commit
597e43967c
2 changed files with 66 additions and 9 deletions
|
|
@ -53,6 +53,22 @@ def json_response(*a, **kwargs) -> HTTPResponse:
|
|||
|
||||
class CustomJSONEncoder(json.JSONEncoder):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# One encoder serializes one response; look up the media directory once, not once per path.
|
||||
self._media_prefix = None
|
||||
|
||||
def _relative_path(self, path: Path) -> str:
|
||||
"""The path relative to the media directory, or unchanged if it is outside of it."""
|
||||
if self._media_prefix is None:
|
||||
self._media_prefix = str(get_media_directory()).rstrip('/') + '/'
|
||||
path = str(path)
|
||||
if path.startswith(self._media_prefix):
|
||||
return path[len(self._media_prefix):]
|
||||
if path == '.' or path + '/' == self._media_prefix:
|
||||
return ''
|
||||
return path
|
||||
|
||||
def default(self, obj):
|
||||
try:
|
||||
if hasattr(obj, '__json__'):
|
||||
|
|
@ -77,15 +93,7 @@ class CustomJSONEncoder(json.JSONEncoder):
|
|||
if hasattr(obj, 'dict'):
|
||||
return obj.dict()
|
||||
elif isinstance(obj, Path):
|
||||
media_directory = get_media_directory()
|
||||
try:
|
||||
path = obj.relative_to(media_directory)
|
||||
except ValueError:
|
||||
# Path may not be absolute.
|
||||
path = obj
|
||||
if str(path) == '.':
|
||||
return ''
|
||||
return str(path)
|
||||
return self._relative_path(obj)
|
||||
return super(CustomJSONEncoder, self).default(obj)
|
||||
except Exception as e:
|
||||
logger.fatal(f'Failed to JSON encode {obj}', exc_info=e)
|
||||
|
|
|
|||
49
wrolpi/test/test_json_encoder.py
Normal file
49
wrolpi/test/test_json_encoder.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""Tests for the API's JSON encoder."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from wrolpi import api_utils
|
||||
from wrolpi.api_utils import CustomJSONEncoder
|
||||
|
||||
|
||||
def encode(obj):
|
||||
return json.loads(json.dumps(obj, cls=CustomJSONEncoder))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path, expected', [
|
||||
('videos/channel/video.mp4', 'videos/channel/video.mp4'), # Inside the media directory.
|
||||
('videos', 'videos'),
|
||||
('', ''), # The media directory itself.
|
||||
])
|
||||
def test_paths_in_media_directory_are_relative(test_directory, path, expected):
|
||||
assert encode(test_directory / path) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path', [
|
||||
'videos/video.mp4', # Already relative.
|
||||
'/opt/wrolpi/main.py', # Outside the media directory.
|
||||
])
|
||||
def test_other_paths_are_unchanged(test_directory, path):
|
||||
assert encode(Path(path)) == path
|
||||
|
||||
|
||||
def test_relative_dot_is_empty(test_directory):
|
||||
assert encode(Path('.')) == ''
|
||||
|
||||
|
||||
def test_sibling_of_media_directory_is_unchanged(test_directory):
|
||||
"""A directory whose name merely starts with the media directory's name is not inside it."""
|
||||
sibling = Path(f'{test_directory}-sibling/video.mp4')
|
||||
assert encode(sibling) == str(sibling)
|
||||
|
||||
|
||||
def test_media_directory_is_looked_up_once_per_response(test_directory):
|
||||
"""A response can hold hundreds of paths; finding the media directory for each one was most of the
|
||||
encoding time."""
|
||||
paths = [test_directory / f'videos/{i}.mp4' for i in range(20)]
|
||||
with mock.patch.object(api_utils, 'get_media_directory', wraps=api_utils.get_media_directory) as lookup:
|
||||
assert encode(paths) == [f'videos/{i}.mp4' for i in range(20)]
|
||||
assert lookup.call_count == 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue