Skip to content

Commit

Permalink
refactor TimeSpan #188
Browse files Browse the repository at this point in the history
  • Loading branch information
cleder committed Jan 14, 2023
1 parent 114f693 commit d2d81db
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 136 deletions.
29 changes: 15 additions & 14 deletions fastkml/kml.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from fastkml.styles import StyleMap
from fastkml.styles import StyleUrl
from fastkml.styles import _StyleSelector
from fastkml.times import KmlDateTime
from fastkml.times import TimeSpan
from fastkml.times import TimeStamp
from fastkml.types import Element
Expand Down Expand Up @@ -216,36 +217,36 @@ def time_stamp(self, timestamp: Optional[TimeStamp]) -> None:
self._timespan = None

@property
def begin(self):
def begin(self) -> Optional[KmlDateTime]:
if self._timespan is not None:
return self._timespan.begin[0]
return self._timespan.begin

@begin.setter
def begin(self, dt):
def begin(self, dt: Optional[KmlDateTime]):
if self._timespan is None:
self._timespan = TimeSpan(begin=dt)
elif self._timespan.begin is None:
self._timespan.begin = [dt, None]
elif dt is None and self._timespan.end is None:
self._timespan = None
else:
self._timespan.begin[0] = dt
if self._timestamp is not None:
self._timespan.begin = dt
if self._timespan and self._timestamp:
logger.warning("Setting a TimeSpan, TimeStamp deleted")
self._timestamp = None

@property
def end(self):
def end(self) -> Optional[KmlDateTime]:
if self._timespan is not None:
return self._timespan.end[0]
return self._timespan.end

@end.setter
def end(self, dt):
def end(self, dt: Optional[KmlDateTime]):
if self._timespan is None:
self._timespan = TimeSpan(end=dt)
elif self._timespan.end is None:
self._timespan.end = [dt, None]
elif dt is None and self._timespan.begin is None:
self._timespan = None
else:
self._timespan.end[0] = dt
if self._timestamp is not None:
self._timespan.end = dt
if self._timespan and self._timestamp:
logger.warning("Setting a TimeSpan, TimeStamp deleted")
self._timestamp = None

Expand Down
100 changes: 20 additions & 80 deletions fastkml/times.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from datetime import date
from datetime import datetime
from typing import Optional
from typing import Tuple
from typing import Union

# note that there are some ISO 8601 timeparsers at pypi
Expand Down Expand Up @@ -99,6 +98,10 @@ def __eq__(self, other: object) -> bool:
else False
)

def __repr__(self) -> str:
"""Return a string representation of the object."""
return f"{self.__class__.__name__}({self.dt!r}, {self.resolution})"

def __str__(self) -> str:
"""Return the KML DateTime string representation of the object."""
if self.resolution == DateTimeResolution.year:
Expand Down Expand Up @@ -148,72 +151,11 @@ class _TimePrimitive(_BaseObject):
https://developers.google.com/kml/documentation/kmlreference#timeprimitive
"""

RESOLUTIONS = ["gYear", "gYearMonth", "date", "dateTime"]

def get_resolution(
self,
dt: Optional[Union[date, datetime]],
resolution: Optional[str] = None,
) -> Optional[str]:
# XXX deprecated use KmlDateTime
if resolution:
if resolution not in self.RESOLUTIONS:
raise ValueError
else:
return resolution
elif isinstance(dt, datetime):
resolution = "dateTime"
elif isinstance(dt, date):
resolution = "date"
else:
resolution = None
return resolution

def parse_str(self, datestr: str) -> Tuple[datetime, str]:
# XXX deprecated use KmlDateTime.parse
if len(datestr) == 4:
year = int(datestr)
return datetime(year, 1, 1), "gYear"
if len(datestr) in {6, 7}:
ym = year_month.match(datestr)
if ym:
year = int(ym.group("year"))
month = int(ym.group("month"))
return datetime(year, month, 1), "gYearMonth"
if len(datestr) in {8, 10}: # 8 is YYYYMMDDS
return dateutil.parser.parse(datestr), "date"
if len(datestr) > 10:
return dateutil.parser.parse(datestr), "dateTime"
raise ValueError

def date_to_string(
self,
dt: Optional[Union[date, datetime]],
resolution: Optional[str] = None,
) -> Optional[str]:
# XXX deprecated use KmlDateTime.__str__
if isinstance(dt, (date, datetime)):
resolution = self.get_resolution(dt, resolution)
if resolution == "gYear":
return dt.strftime("%Y")
elif resolution == "gYearMonth":
return dt.strftime("%Y-%m")
elif resolution == "date":
return (
dt.date().isoformat()
if isinstance(dt, datetime)
else dt.isoformat()
)
elif resolution == "dateTime":
return dt.isoformat()
return None


class TimeStamp(_TimePrimitive):
"""Represents a single moment in time."""

__name__ = "TimeStamp"
timestamp: Optional[Tuple[datetime, str]] = None

def __init__(
self,
Expand Down Expand Up @@ -244,47 +186,45 @@ class TimeSpan(_TimePrimitive):
"""Represents an extent in time bounded by begin and end dateTimes."""

__name__ = "TimeSpan"
begin = None
end = None

def __init__(
self,
ns: Optional[str] = None,
id: Optional[str] = None,
target_id: Optional[str] = None,
begin: Optional[Union[date, datetime]] = None,
begin_res: None = None,
end: Optional[Union[date, datetime]] = None,
end_res: None = None,
begin: Optional[KmlDateTime] = None,
end: Optional[KmlDateTime] = None,
) -> None:
super().__init__(ns=ns, id=id, target_id=target_id)
if begin:
resolution = self.get_resolution(begin, begin_res)
self.begin = [begin, resolution]
if end:
resolution = self.get_resolution(end, end_res)
self.end = [end, resolution]
self.begin = begin
self.end = end

def from_element(self, element: Element) -> None:
super().from_element(element)
begin = element.find(f"{self.ns}begin")
if begin is not None:
self.begin = self.parse_str(begin.text)
self.begin = KmlDateTime.parse(begin.text)
end = element.find(f"{self.ns}end")
if end is not None:
self.end = self.parse_str(end.text)
self.end = KmlDateTime.parse(end.text)

def etree_element(self) -> Element:
element = super().etree_element()
if self.begin is not None:
text = self.date_to_string(*self.begin)
text = str(self.begin)
if text:
begin = config.etree.SubElement(element, f"{self.ns}begin")
begin = config.etree.SubElement( # type: ignore[attr-defined]
element,
f"{self.ns}begin",
)
begin.text = text
if self.end is not None:
text = self.date_to_string(*self.end)
text = str(self.end)
if text:
end = config.etree.SubElement(element, f"{self.ns}end")
end = config.etree.SubElement( # type: ignore[attr-defined]
element,
f"{self.ns}end",
)
end.text = text
if self.begin == self.end is None:
raise ValueError("Either begin, end or both must be set")
Expand Down
34 changes: 17 additions & 17 deletions fastkml/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import datetime
import logging
from typing import Optional
from typing import Union

import fastkml.config as config
import fastkml.gx as gx
from fastkml.base import _BaseObject
from fastkml.times import KmlDateTime
from fastkml.times import TimeSpan
from fastkml.times import TimeStamp
from fastkml.types import Element
Expand Down Expand Up @@ -99,36 +99,36 @@ def timestamp(self, timestamp: Optional[TimeStamp]) -> None:
self._timespan = None

@property
def begin(self) -> Optional[datetime.datetime]:
if self._timespan is None:
return None
return self._timespan.begin[0]
def begin(self) -> Optional[KmlDateTime]:
if self._timespan is not None:
return self._timespan.begin

@begin.setter
def begin(self, dt) -> None:
def begin(self, dt: Optional[KmlDateTime]):
if self._timespan is None:
self._timespan = TimeSpan(begin=dt)
elif self._timespan.begin is None:
self._timespan.begin = [dt, None]
elif dt is None and self._timespan.end is None:
self._timespan = None
else:
self._timespan.begin[0] = dt
if self._timestamp is not None:
self._timespan.begin = dt
if self._timespan and self._timestamp:
logger.warning("Setting a TimeSpan, TimeStamp deleted")
self._timestamp = None

@property
def end(self) -> Optional[datetime.datetime]:
return None if self._timespan is None else self._timespan.end[0]
def end(self) -> Optional[KmlDateTime]:
if self._timespan is not None:
return self._timespan.end

@end.setter
def end(self, dt) -> None:
def end(self, dt: Optional[KmlDateTime]):
if self._timespan is None:
self._timespan = TimeSpan(end=dt)
elif self._timespan.end is None:
self._timespan.end = [dt, None]
elif dt is None and self._timespan.begin is None:
self._timespan = None
else:
self._timespan.end[0] = dt
if self._timestamp is not None:
self._timespan.end = dt
if self._timespan and self._timestamp:
logger.warning("Setting a TimeSpan, TimeStamp deleted")
self._timestamp = None

Expand Down
38 changes: 19 additions & 19 deletions tests/times_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,19 +194,19 @@ def test_timestamp(self):
assert "2000-01-01" in str(ts.to_string())

def test_timespan(self):
now = datetime.datetime.now()
y2k = datetime.datetime(2000, 1, 1)
now = KmlDateTime(datetime.datetime.now())
y2k = KmlDateTime(datetime.datetime(2000, 1, 1))
ts = kml.TimeSpan(end=now, begin=y2k)
assert ts.end == [now, "dateTime"]
assert ts.begin == [y2k, "dateTime"]
assert ts.end == now
assert ts.begin == y2k
assert "TimeSpan>" in str(ts.to_string())
assert "begin>" in str(ts.to_string())
assert "end>" in str(ts.to_string())
assert now.isoformat() in str(ts.to_string())
assert y2k.isoformat() in str(ts.to_string())
assert now.dt.isoformat() in str(ts.to_string())
assert y2k.dt.isoformat() in str(ts.to_string())
ts.end = None
assert now.isoformat() not in str(ts.to_string())
assert y2k.isoformat() in str(ts.to_string())
assert now.dt.isoformat() not in str(ts.to_string())
assert y2k.dt.isoformat() in str(ts.to_string())
ts.begin = None
pytest.raises(ValueError, ts.to_string)

Expand All @@ -226,12 +226,12 @@ def test_feature_timestamp(self):

def test_feature_timespan(self):
now = datetime.datetime.now()
y2k = datetime.date(2000, 1, 1)
y2k = datetime.datetime(2000, 1, 1)
f = kml.Document()
f.begin = y2k
f.end = now
assert f.begin == y2k
assert f.end == now
f.begin = KmlDateTime(y2k)
f.end = KmlDateTime(now)
assert f.begin == KmlDateTime(y2k)
assert f.end == KmlDateTime(now)
assert now.isoformat() in str(f.to_string())
assert "2000-01-01" in str(f.to_string())
assert "TimeSpan>" in str(f.to_string())
Expand All @@ -250,8 +250,8 @@ def test_feature_timespan_stamp(self):
now = datetime.datetime.now()
y2k = datetime.date(2000, 1, 1)
f = kml.Document()
f.begin = y2k
f.end = now
f.begin = KmlDateTime(y2k)
f.end = KmlDateTime(now)
assert now.isoformat() in str(f.to_string())
assert "2000-01-01" in str(f.to_string())
assert "TimeSpan>" in str(f.to_string())
Expand Down Expand Up @@ -358,10 +358,10 @@ def test_read_timespan(self):
"""

ts.from_string(doc)
assert ts.begin[1] == "date"
assert ts.begin[0] == datetime.datetime(1876, 8, 1, 0, 0)
assert ts.end[1] == "dateTime"
assert ts.end[0] == datetime.datetime(1997, 7, 16, 7, 30, 15, tzinfo=tzutc())
assert ts.begin.resolution == DateTimeResolution.date
assert ts.begin.dt == datetime.datetime(1876, 8, 1, 0, 0)
assert ts.end.resolution == DateTimeResolution.datetime
assert ts.end.dt == datetime.datetime(1997, 7, 16, 7, 30, 15, tzinfo=tzutc())

def test_featurefromstring(self):
d = kml.Document(ns="")
Expand Down
12 changes: 6 additions & 6 deletions tests/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def test_create_camera(self) -> None:
"""Test the creation of a camera."""
time_span = times.TimeSpan(
id="time-span-id",
begin=datetime.datetime(2019, 1, 1),
end=datetime.datetime(2019, 1, 2),
begin=times.KmlDateTime(datetime.datetime(2019, 1, 1)),
end=times.KmlDateTime(datetime.datetime(2019, 1, 2)),
)

camera = views.Camera(
Expand All @@ -57,8 +57,8 @@ def test_create_camera(self) -> None:
assert camera.longitude == 60
assert camera.id == "cam-id"
assert camera.target_id == "target-cam-id"
assert camera.begin == datetime.datetime(2019, 1, 1)
assert camera.end == datetime.datetime(2019, 1, 2)
assert camera.begin == times.KmlDateTime(datetime.datetime(2019, 1, 1))
assert camera.end == times.KmlDateTime(datetime.datetime(2019, 1, 2))
assert camera.to_string()

def test_camera_read(self) -> None:
Expand Down Expand Up @@ -92,8 +92,8 @@ def test_camera_read(self) -> None:
assert camera.longitude == 60
assert camera.id == "cam-id"
assert camera.target_id == "target-cam-id"
assert camera.begin == datetime.datetime(2019, 1, 1)
assert camera.end == datetime.datetime(2019, 1, 2)
assert camera.begin == times.KmlDateTime(datetime.datetime(2019, 1, 1))
assert camera.end == times.KmlDateTime(datetime.datetime(2019, 1, 2))

def test_create_look_at(self) -> None:
time_stamp = times.TimeStamp(
Expand Down

0 comments on commit d2d81db

Please sign in to comment.