Add local timezone to UTC converter

This commit is contained in:
Joakim Soderlund 2019-03-20 14:08:41 +01:00
parent 314923a4de
commit df4c820ca1
4 changed files with 178 additions and 34 deletions

View file

@ -25,10 +25,12 @@ Converter module.
from .base import Converter from .base import Converter
from .alpha_beta import AlphaBetaConverter from .alpha_beta import AlphaBetaConverter
from .json_fpub import JsonFpubConverter from .json_fpub import JsonFpubConverter
from .local_utc import LocalUtcConverter
__all__ = ( __all__ = (
'Converter', 'Converter',
'AlphaBetaConverter', 'AlphaBetaConverter',
'JsonFpubConverter', 'JsonFpubConverter',
'LocalUtcConverter',
) )

View file

@ -23,21 +23,19 @@ JSON to FPUB converter for story data.
import json import json
from copy import deepcopy
from io import BytesIO 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 from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
import arrow
from jinja2 import Environment, PackageLoader from jinja2 import Environment, PackageLoader
from fimfarchive.flavors import DataFormat, MetaFormat from fimfarchive.flavors import DataFormat, MetaFormat
from fimfarchive.stories import Story from fimfarchive.stories import Story
from fimfarchive.utils import JayWalker
from fimfarchive.fetchers.fimfiction2 import BetaFormatVerifier from fimfarchive.fetchers.fimfiction2 import BetaFormatVerifier
from ..base import Converter from ..base import Converter
from ..local_utc import LocalUtcConverter
__all__ = ( __all__ = (
@ -49,29 +47,6 @@ MIMETYPE = 'application/epub+zip'
PACKAGE = __package__.rsplit('.', 1) 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: class StoryRenderer:
""" """
Renders story data. Renders story data.
@ -89,7 +64,6 @@ class StoryRenderer:
self.book_opf = env.get_template('book.opf') self.book_opf = env.get_template('book.opf')
self.book_ncx = env.get_template('book.ncx') self.book_ncx = env.get_template('book.ncx')
self.date_normalizer = DateNormalizer()
self.verify_meta = BetaFormatVerifier.from_meta_params() self.verify_meta = BetaFormatVerifier.from_meta_params()
self.verify_data = BetaFormatVerifier.from_data_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, meta['chapter_number'])
self.verify_index(index, data['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]]: def iter_content(self, story: Story) -> Iterator[Tuple[str, str]]:
""" """
@ -164,11 +138,8 @@ class StoryRenderer:
yield path, self.chapter_html.render(chapter) yield path, self.chapter_html.render(chapter)
meta = deepcopy(story.meta) yield 'book.opf', self.book_opf.render(story.meta)
self.date_normalizer.walk(meta) yield 'book.ncx', self.book_ncx.render(story.meta)
yield 'book.opf', self.book_opf.render(meta)
yield 'book.ncx', self.book_ncx.render(meta)
def __call__(self, story: Story) -> bytes: def __call__(self, story: Story) -> bytes:
""" """
@ -192,6 +163,7 @@ class JsonFpubConverter(Converter):
def __init__(self) -> None: def __init__(self) -> None:
self.render = StoryRenderer() self.render = StoryRenderer()
self.normalize = LocalUtcConverter()
def __call__(self, story: Story) -> Story: def __call__(self, story: Story) -> Story:
if DataFormat.JSON not in story.flavors: if DataFormat.JSON not in story.flavors:
@ -200,6 +172,7 @@ class JsonFpubConverter(Converter):
if MetaFormat.BETA not in story.flavors: if MetaFormat.BETA not in story.flavors:
raise ValueError(f"Missing flavor: {MetaFormat.BETA}") raise ValueError(f"Missing flavor: {MetaFormat.BETA}")
story = self.normalize(story)
data = self.render(story) data = self.render(story)
flavors = set(story.flavors) flavors = set(story.flavors)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
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)

View file

@ -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 <http://www.gnu.org/licenses/>.
#
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