core/compat: move fromisoformat to .core.compat module

This commit is contained in:
Dima Gerasimov 2023-11-19 22:45:02 +00:00 committed by karlicoss
parent 09e0f66892
commit a843407e40
8 changed files with 69 additions and 34 deletions

View file

@ -23,7 +23,7 @@ def inputs() -> Sequence[Path]:
from .core import dataclass, Json, PathIsh, datetime_aware from .core import dataclass, Json, PathIsh, datetime_aware
from .core.common import isoparse from .core.compat import fromisoformat
@dataclass @dataclass
@ -39,6 +39,7 @@ class Entry:
@property @property
def dt(self) -> datetime_aware: def dt(self) -> datetime_aware:
# contains utc already # contains utc already
# TODO after python>=3.11, could just use fromisoformat
ds = self.json['date'] ds = self.json['date']
elen = 27 elen = 27
lds = len(ds) lds = len(ds)
@ -46,10 +47,10 @@ class Entry:
# ugh. sometimes contains less that 6 decimal points # ugh. sometimes contains less that 6 decimal points
ds = ds[:-1] + '0' * (elen - lds) + 'Z' ds = ds[:-1] + '0' * (elen - lds) + 'Z'
elif lds > elen: elif lds > elen:
# ahd sometimes more... # and sometimes more...
ds = ds[:elen - 1] + 'Z' ds = ds[:elen - 1] + 'Z'
return isoparse(ds) return fromisoformat(ds)
@property @property
def active(self) -> Optional[str]: def active(self) -> Optional[str]:

View file

@ -313,20 +313,18 @@ class classproperty(Generic[_R]):
# def __get__(self) -> _R: # def __get__(self) -> _R:
# return self.f() # return self.f()
# TODO deprecate in favor of datetime_aware # for now just serves documentation purposes... but one day might make it statically verifiable where possible?
tzdatetime = datetime # TODO e.g. maybe use opaque mypy alias?
datetime_naive = datetime
datetime_aware = datetime
# TODO doctests? # TODO deprecate
def isoparse(s: str) -> tzdatetime: tzdatetime = datetime_aware
"""
Parses timestamps formatted like 2020-05-01T10:32:02.925961Z
""" # TODO deprecate (although could be used in modules)
# TODO could use dateutil? but it's quite slow as far as I remember.. from .compat import fromisoformat as isoparse
# TODO support non-utc.. somehow?
assert s.endswith('Z'), s
s = s[:-1] + '+00:00'
return datetime.fromisoformat(s)
import re import re
@ -590,12 +588,6 @@ def asdict(thing: Any) -> Json:
raise TypeError(f'Could not convert object {thing} to dict') 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: 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 # 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 ... # NOTE: if we use overlay, name can be smth like my.origg.my.core.cachew ...

View file

@ -76,3 +76,42 @@ if sys.version_info[:2] <= (3, 9):
return lo return lo
else: else:
from bisect import bisect_left 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,
# )

View file

@ -24,7 +24,7 @@ from .query import (
ET, ET,
) )
from .common import isoparse from .compat import fromisoformat
timedelta_regex = re.compile(r"^((?P<weeks>[\.\d]+?)w)?((?P<days>[\.\d]+?)d)?((?P<hours>[\.\d]+?)h)?((?P<minutes>[\.\d]+?)m)?((?P<seconds>[\.\d]+?)s)?$") timedelta_regex = re.compile(r"^((?P<weeks>[\.\d]+?)w)?((?P<days>[\.\d]+?)d)?((?P<hours>[\.\d]+?)h)?((?P<minutes>[\.\d]+?)m)?((?P<seconds>[\.\d]+?)s)?$")
@ -78,7 +78,7 @@ def parse_datetime_float(date_str: str) -> float:
except ValueError: except ValueError:
pass pass
try: try:
return isoparse(ds).timestamp() return fromisoformat(ds).timestamp()
except (AssertionError, ValueError): except (AssertionError, ValueError):
pass pass

View file

@ -42,7 +42,7 @@ from typing import List, Dict, Iterable, NamedTuple, Sequence, Optional
import json import json
from .core import LazyLogger, Json, Res 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.error import echain, sort_res_by
from .core.konsume import wrap, Zoomable, Wdict from .core.konsume import wrap, Zoomable, Wdict
@ -145,7 +145,7 @@ class Loader:
cmap[hlid] = ccs cmap[hlid] = ccs
ccs.append(Comment( ccs.append(Comment(
cid=cid.value, cid=cid.value,
created=isoparse(crt.value), created=fromisoformat(crt.value),
text=html.value, # TODO perhaps coonvert from html to text or org? text=html.value, # TODO perhaps coonvert from html to text or org?
)) ))
v.consume() v.consume()
@ -183,7 +183,7 @@ class Loader:
yield Highlight( yield Highlight(
hid=hid, hid=hid,
created=isoparse(crt), created=fromisoformat(crt),
selection=text, selection=text,
comments=tuple(comments), comments=tuple(comments),
tags=tuple(htags), tags=tuple(htags),
@ -221,7 +221,7 @@ class Loader:
path = Path(config.polar_dir) / 'stash' / filename path = Path(config.polar_dir) / 'stash' / filename
yield Book( yield Book(
created=isoparse(added), created=fromisoformat(added),
uid=self.uid, uid=self.uid,
path=path, path=path,
title=title, title=title,

View file

@ -7,7 +7,8 @@ from my.config import feedbin as config
from pathlib import Path from pathlib import Path
from typing import Sequence 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 from .common import Subscription
@ -22,7 +23,7 @@ def parse_file(f: Path):
raw = json.loads(f.read_text()) raw = json.loads(f.read_text())
for r in raw: for r in raw:
yield Subscription( yield Subscription(
created_at=isoparse(r['created_at']), created_at=fromisoformat(r['created_at']),
title=r['title'], title=r['title'],
url=r['site_url'], url=r['site_url'],
id=r['id'], id=r['id'],

View file

@ -11,7 +11,8 @@ from pathlib import Path
from typing import Iterable from typing import Iterable
from .core import Res, get_files 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] import tcxparser # type: ignore[import-untyped]
@ -44,7 +45,7 @@ def _parse(f: Path) -> Workout:
return { return {
'id' : f.name, # not sure? 'id' : f.name, # not sure?
'start_time' : isoparse(tcx.started_at), 'start_time' : fromisoformat(tcx.started_at),
'duration' : timedelta(seconds=tcx.duration), 'duration' : timedelta(seconds=tcx.duration),
'sport' : sport, 'sport' : sport,
'heart_rate_avg': tcx.hr_avg, 'heart_rate_avg': tcx.hr_avg,
@ -58,7 +59,7 @@ def _parse(f: Path) -> Workout:
# tcx.hr_values(), # tcx.hr_values(),
# # todo cadence? # # todo cadence?
# ): # ):
# t = isoparse(ts) # t = fromisoformat(ts)
def workouts() -> Iterable[Res[Workout]]: def workouts() -> Iterable[Res[Workout]]:

View file

@ -16,7 +16,8 @@ config = make_config(stackexchange)
# TODO just merge all of them and then filter?.. not sure # 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 typing import NamedTuple, Iterable
from datetime import datetime from datetime import datetime
class Vote(NamedTuple): class Vote(NamedTuple):
@ -25,7 +26,7 @@ class Vote(NamedTuple):
@property @property
def when(self) -> datetime: def when(self) -> datetime:
return isoparse(self.j['eventTime']) return fromisoformat(self.j['eventTime'])
# todo Url return type? # todo Url return type?
@property @property