mirror of
https://github.com/JockeTF/fimfarchive.git
synced 2024-11-25 22:47:59 +01:00
Add local timezone to UTC converter
This commit is contained in:
parent
314923a4de
commit
df4c820ca1
4 changed files with 178 additions and 34 deletions
|
@ -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',
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
71
fimfarchive/converters/local_utc.py
Normal file
71
fimfarchive/converters/local_utc.py
Normal 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)
|
98
tests/converters/test_local_utc.py
Normal file
98
tests/converters/test_local_utc.py
Normal 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
|
Loading…
Reference in a new issue