diff --git a/fimfarchive/converters/__init__.py b/fimfarchive/converters/__init__.py index a072a63..813a406 100644 --- a/fimfarchive/converters/__init__.py +++ b/fimfarchive/converters/__init__.py @@ -25,10 +25,12 @@ Converter module. from .base import Converter from .alpha_beta import AlphaBetaConverter from .json_fpub import JsonFpubConverter +from .local_utc import LocalUtcConverter __all__ = ( 'Converter', 'AlphaBetaConverter', 'JsonFpubConverter', + 'LocalUtcConverter', ) diff --git a/fimfarchive/converters/json_fpub/__init__.py b/fimfarchive/converters/json_fpub/__init__.py index 212ea78..de7cf24 100644 --- a/fimfarchive/converters/json_fpub/__init__.py +++ b/fimfarchive/converters/json_fpub/__init__.py @@ -23,21 +23,19 @@ JSON to FPUB converter for story data. import json -from copy import deepcopy from io import BytesIO -from typing import Any, Dict, Iterator, Optional, Tuple +from typing import Any, Dict, Iterator, Tuple from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED -import arrow from jinja2 import Environment, PackageLoader from fimfarchive.flavors import DataFormat, MetaFormat from fimfarchive.stories import Story -from fimfarchive.utils import JayWalker from fimfarchive.fetchers.fimfiction2 import BetaFormatVerifier from ..base import Converter +from ..local_utc import LocalUtcConverter __all__ = ( @@ -49,29 +47,6 @@ MIMETYPE = 'application/epub+zip' PACKAGE = __package__.rsplit('.', 1) -class DateNormalizer(JayWalker): - """ - Normalizes timezones of date values to UTC. - """ - - def handle(self, data, key, value) -> None: - if str(key).startswith('date_'): - data[key] = self.normalize(value) - else: - self.walk(value) - - def normalize(self, value: Optional[str]) -> Optional[str]: - """ - Normalizes a single date value. - """ - parsed = arrow.get(value or 0) - - if parsed.timestamp == 0: - return None - - return parsed.to('utc').isoformat() - - class StoryRenderer: """ Renders story data. @@ -89,7 +64,6 @@ class StoryRenderer: self.book_opf = env.get_template('book.opf') self.book_ncx = env.get_template('book.ncx') - self.date_normalizer = DateNormalizer() self.verify_meta = BetaFormatVerifier.from_meta_params() self.verify_data = BetaFormatVerifier.from_data_params() @@ -146,7 +120,7 @@ class StoryRenderer: self.verify_index(index, meta['chapter_number']) self.verify_index(index, data['chapter_number']) - yield {**meta, **data} + yield {**data, **meta} def iter_content(self, story: Story) -> Iterator[Tuple[str, str]]: """ @@ -164,11 +138,8 @@ class StoryRenderer: yield path, self.chapter_html.render(chapter) - meta = deepcopy(story.meta) - self.date_normalizer.walk(meta) - - yield 'book.opf', self.book_opf.render(meta) - yield 'book.ncx', self.book_ncx.render(meta) + yield 'book.opf', self.book_opf.render(story.meta) + yield 'book.ncx', self.book_ncx.render(story.meta) def __call__(self, story: Story) -> bytes: """ @@ -192,6 +163,7 @@ class JsonFpubConverter(Converter): def __init__(self) -> None: self.render = StoryRenderer() + self.normalize = LocalUtcConverter() def __call__(self, story: Story) -> Story: if DataFormat.JSON not in story.flavors: @@ -200,6 +172,7 @@ class JsonFpubConverter(Converter): if MetaFormat.BETA not in story.flavors: raise ValueError(f"Missing flavor: {MetaFormat.BETA}") + story = self.normalize(story) data = self.render(story) flavors = set(story.flavors) diff --git a/fimfarchive/converters/local_utc.py b/fimfarchive/converters/local_utc.py new file mode 100644 index 0000000..d1b4eab --- /dev/null +++ b/fimfarchive/converters/local_utc.py @@ -0,0 +1,71 @@ +""" +Local timezone to UTC converter. +""" + + +# +# Fimfarchive, preserves stories from Fimfiction. +# Copyright (C) 2019 Joakim Soderlund +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + + +from copy import deepcopy +from typing import Any, Optional + +import arrow + +from fimfarchive.stories import Story +from fimfarchive.utils import JayWalker + +from .base import Converter + + +class DateNormalizer(JayWalker): + """ + Normalizes timezones of date values to UTC. + """ + + def handle(self, data, key, value) -> None: + if str(key).startswith('date_'): + data[key] = self.normalize(value) + else: + self.walk(value) + + def normalize(self, value: Any) -> Optional[str]: + """ + Normalizes a single date value. + """ + parsed = arrow.get(value or 0) + + if parsed.timestamp == 0: + return None + + return parsed.to('utc').isoformat() + + +class LocalUtcConverter(Converter): + """ + Converts date strings to UTC. + """ + + def __init__(self) -> None: + self.normalizer = DateNormalizer() + + def __call__(self, story: Story) -> Story: + meta = deepcopy(story.meta) + self.normalizer.walk(meta) + + return story.merge(meta=meta) diff --git a/tests/converters/test_local_utc.py b/tests/converters/test_local_utc.py new file mode 100644 index 0000000..f057e3b --- /dev/null +++ b/tests/converters/test_local_utc.py @@ -0,0 +1,98 @@ +""" +Local timezone to UTC converter tests. +""" + + +# +# Fimfarchive, preserves stories from Fimfiction. +# Copyright (C) 2019 Joakim Soderlund +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + + +import json +import pytest +from copy import deepcopy + +from fimfarchive.converters import LocalUtcConverter + + +class TestLocalUtcConverter: + """ + LocalUtcConverter tests. + """ + + @pytest.fixture(params=[ + ('2019-03-20T11:27:58+00:00', '2019-03-20T11:27:58+00:00'), + ('2019-03-20T12:29:15+01:00', '2019-03-20T11:29:15+00:00'), + ('1970-01-01T00:00:00+00:00', None), + ('1970-01-01T01:00:00+01:00', None), + (None, None), + ]) + def date_pair(self, request): + """ + Returns a date pair. + """ + local, utc = request.param + + return local, utc + + @pytest.fixture(params=[ + '{"a":{"b":{"c":{"d":"e"}}}}', + '{"a":{"b":{"c":{"date_x":?}}}}', + '{"a":{"b":{"c":{"date_x":?,"date_y":?}}}}', + '{"a":{"b":{"c":{"date_x":?},"date_y":?}}}', + '{"a":{"b":{"c":{"date_x":?}},"date_y":?}}', + '{"date_x":?,"kittens":"2019-03-20T13:06:13+01:00","a":{"date_x":?}}', + '{"date_x":?,"kittens":"2019-03-20T13:06:13+01:00","date_y":?}', + ]) + def meta_pair(self, request, date_pair): + """ + Returns a meta pair. + """ + template = request.param + local_date, utc_date = date_pair + + local_value = json.dumps(local_date) + local_json = template.replace('?', local_value) + local_meta = json.loads(local_json) + + utc_value = json.dumps(utc_date) + utc_json = template.replace('?', utc_value) + utc_meta = json.loads(utc_json) + + return local_meta, utc_meta + + @pytest.fixture + def converter(self): + """ + Returns a converter instance. + """ + return LocalUtcConverter() + + def test_conversion(self, converter, story, meta_pair): + """ + Tests local to UTC conversion. + """ + local_meta, utc_meta = meta_pair + + local_story = story.merge(meta=local_meta) + utc_story = story.merge(meta=utc_meta) + + clone = deepcopy(local_story) + converted = converter(local_story) + + assert clone.meta == local_story.meta + assert utc_story.meta == converted.meta