diff --git a/my/arbtt.py b/my/arbtt.py index 5683515..941a05f 100644 --- a/my/arbtt.py +++ b/my/arbtt.py @@ -23,7 +23,7 @@ def inputs() -> Sequence[Path]: from .core import dataclass, Json, PathIsh, datetime_aware -from .core.common import isoparse +from .core.compat import fromisoformat @dataclass @@ -39,6 +39,7 @@ class Entry: @property def dt(self) -> datetime_aware: # contains utc already + # TODO after python>=3.11, could just use fromisoformat ds = self.json['date'] elen = 27 lds = len(ds) @@ -46,10 +47,10 @@ class Entry: # ugh. sometimes contains less that 6 decimal points ds = ds[:-1] + '0' * (elen - lds) + 'Z' elif lds > elen: - # ahd sometimes more... + # and sometimes more... ds = ds[:elen - 1] + 'Z' - return isoparse(ds) + return fromisoformat(ds) @property def active(self) -> Optional[str]: diff --git a/my/core/common.py b/my/core/common.py index 85b9386..f1441a9 100644 --- a/my/core/common.py +++ b/my/core/common.py @@ -313,20 +313,18 @@ class classproperty(Generic[_R]): # def __get__(self) -> _R: # return self.f() -# TODO deprecate in favor of datetime_aware -tzdatetime = datetime +# for now just serves documentation purposes... but one day might make it statically verifiable where possible? +# TODO e.g. maybe use opaque mypy alias? +datetime_naive = datetime +datetime_aware = datetime -# TODO doctests? -def isoparse(s: str) -> tzdatetime: - """ - Parses timestamps formatted like 2020-05-01T10:32:02.925961Z - """ - # TODO could use dateutil? but it's quite slow as far as I remember.. - # TODO support non-utc.. somehow? - assert s.endswith('Z'), s - s = s[:-1] + '+00:00' - return datetime.fromisoformat(s) +# TODO deprecate +tzdatetime = datetime_aware + + +# TODO deprecate (although could be used in modules) +from .compat import fromisoformat as isoparse import re @@ -590,12 +588,6 @@ def asdict(thing: Any) -> Json: raise TypeError(f'Could not convert object {thing} to dict') -# for now just serves documentation purposes... but one day might make it statically verifiable where possible? -# TODO e.g. maybe use opaque mypy alias? -datetime_naive = datetime -datetime_aware = datetime - - def assert_subpackage(name: str) -> None: # can lead to some unexpected issues if you 'import cachew' which being in my/core directory.. so let's protect against it # NOTE: if we use overlay, name can be smth like my.origg.my.core.cachew ... diff --git a/my/core/compat.py b/my/core/compat.py index 48e194b..9cdea27 100644 --- a/my/core/compat.py +++ b/my/core/compat.py @@ -76,3 +76,42 @@ if sys.version_info[:2] <= (3, 9): return lo else: from bisect import bisect_left + + +from datetime import datetime +if sys.version_info[:2] >= (3, 11): + fromisoformat = datetime.fromisoformat +else: + def fromisoformat(date_string: str) -> datetime: + # didn't support Z as "utc" before 3.11 + if date_string.endswith('Z'): + # NOTE: can be removed from 3.11? + # https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat + date_string = date_string[:-1] + '+00:00' + return datetime.fromisoformat(date_string) + + +def test_fromisoformat() -> None: + from datetime import timezone + + # feedbin has this format + assert fromisoformat('2020-05-01T10:32:02.925961Z') == datetime( + 2020, 5, 1, 10, 32, 2, 925961, timezone.utc, + ) + + # polar has this format + assert fromisoformat('2018-11-28T22:04:01.304Z') == datetime( + 2018, 11, 28, 22, 4, 1, 304000, timezone.utc, + ) + + # stackexchange, runnerup has this format + assert fromisoformat('2020-11-30T00:53:12Z') == datetime( + 2020, 11, 30, 0, 53, 12, 0, timezone.utc, + ) + + # arbtt has this format (sometimes less/more than 6 digits in milliseconds) + # TODO doesn't work atm, not sure if really should be supported... + # maybe should have flags for weird formats? + # assert isoparse('2017-07-18T18:59:38.21731Z') == datetime( + # 2017, 7, 18, 18, 59, 38, 217310, timezone.utc, + # ) diff --git a/my/core/query_range.py b/my/core/query_range.py index afde933..dfb9e55 100644 --- a/my/core/query_range.py +++ b/my/core/query_range.py @@ -24,7 +24,7 @@ from .query import ( ET, ) -from .common import isoparse +from .compat import fromisoformat timedelta_regex = re.compile(r"^((?P[\.\d]+?)w)?((?P[\.\d]+?)d)?((?P[\.\d]+?)h)?((?P[\.\d]+?)m)?((?P[\.\d]+?)s)?$") @@ -78,7 +78,7 @@ def parse_datetime_float(date_str: str) -> float: except ValueError: pass try: - return isoparse(ds).timestamp() + return fromisoformat(ds).timestamp() except (AssertionError, ValueError): pass diff --git a/my/polar.py b/my/polar.py index fe59d00..cd2c719 100644 --- a/my/polar.py +++ b/my/polar.py @@ -42,7 +42,7 @@ from typing import List, Dict, Iterable, NamedTuple, Sequence, Optional import json from .core import LazyLogger, Json, Res -from .core.common import isoparse +from .core.compat import fromisoformat from .core.error import echain, sort_res_by from .core.konsume import wrap, Zoomable, Wdict @@ -145,7 +145,7 @@ class Loader: cmap[hlid] = ccs ccs.append(Comment( cid=cid.value, - created=isoparse(crt.value), + created=fromisoformat(crt.value), text=html.value, # TODO perhaps coonvert from html to text or org? )) v.consume() @@ -183,7 +183,7 @@ class Loader: yield Highlight( hid=hid, - created=isoparse(crt), + created=fromisoformat(crt), selection=text, comments=tuple(comments), tags=tuple(htags), @@ -221,7 +221,7 @@ class Loader: path = Path(config.polar_dir) / 'stash' / filename yield Book( - created=isoparse(added), + created=fromisoformat(added), uid=self.uid, path=path, title=title, diff --git a/my/rss/feedbin.py b/my/rss/feedbin.py index 8ba25b8..6160abc 100644 --- a/my/rss/feedbin.py +++ b/my/rss/feedbin.py @@ -7,7 +7,8 @@ from my.config import feedbin as config from pathlib import Path from typing import Sequence -from ..core.common import listify, get_files, isoparse +from ..core.common import listify, get_files +from ..core.compat import fromisoformat from .common import Subscription @@ -22,7 +23,7 @@ def parse_file(f: Path): raw = json.loads(f.read_text()) for r in raw: yield Subscription( - created_at=isoparse(r['created_at']), + created_at=fromisoformat(r['created_at']), title=r['title'], url=r['site_url'], id=r['id'], diff --git a/my/runnerup.py b/my/runnerup.py index f12d9b3..ca09466 100644 --- a/my/runnerup.py +++ b/my/runnerup.py @@ -11,7 +11,8 @@ from pathlib import Path from typing import Iterable from .core import Res, get_files -from .core.common import isoparse, Json +from .core.common import Json +from .core.compat import fromisoformat import tcxparser # type: ignore[import-untyped] @@ -44,7 +45,7 @@ def _parse(f: Path) -> Workout: return { 'id' : f.name, # not sure? - 'start_time' : isoparse(tcx.started_at), + 'start_time' : fromisoformat(tcx.started_at), 'duration' : timedelta(seconds=tcx.duration), 'sport' : sport, 'heart_rate_avg': tcx.hr_avg, @@ -58,7 +59,7 @@ def _parse(f: Path) -> Workout: # tcx.hr_values(), # # todo cadence? # ): - # t = isoparse(ts) + # t = fromisoformat(ts) def workouts() -> Iterable[Res[Workout]]: diff --git a/my/stackexchange/gdpr.py b/my/stackexchange/gdpr.py index 18b2b4d..2f3b98d 100644 --- a/my/stackexchange/gdpr.py +++ b/my/stackexchange/gdpr.py @@ -16,7 +16,8 @@ config = make_config(stackexchange) # TODO just merge all of them and then filter?.. not sure -from ..core.common import Json, isoparse +from ..core.common import Json +from ..core.compat import fromisoformat from typing import NamedTuple, Iterable from datetime import datetime class Vote(NamedTuple): @@ -25,7 +26,7 @@ class Vote(NamedTuple): @property def when(self) -> datetime: - return isoparse(self.j['eventTime']) + return fromisoformat(self.j['eventTime']) # todo Url return type? @property