get rid of porg dependency, use orgparse directly

This commit is contained in:
Dima Gerasimov 2020-11-06 04:04:05 +00:00 committed by karlicoss
parent 62e1bdc39a
commit a6e5908e6d
8 changed files with 101 additions and 54 deletions

View file

@ -7,11 +7,11 @@ from typing import Iterable, NamedTuple, Optional
from ..core.common import listify from ..core.common import listify
from ..core.error import Res, echain from ..core.error import Res, echain
from ..core.orgmode import parse_org_datetime from ..core.orgmode import parse_org_datetime, one_table
import pandas as pd # type: ignore import pandas as pd # type: ignore
import porg import orgparse
from my.config import blood as config from my.config import blood as config
@ -47,14 +47,13 @@ def try_float(s: str) -> Optional[float]:
return None return None
return float(x) return float(x)
def glucose_ketones_data() -> Iterable[Result]: def glucose_ketones_data() -> Iterable[Result]:
o = porg.Org.from_file(str(config.blood_log)) o = orgparse.load(config.blood_log)
tbl = o.xpath('//table') tbl = one_table(o)
# todo some sort of sql-like interface for org tables might be ideal? # todo some sort of sql-like interface for org tables might be ideal?
for l in tbl.lines: for l in tbl.as_dicts:
kets = l['ket'].strip() kets = l['ket']
glus = l['glu'].strip() glus = l['glu']
extra = l['notes'] extra = l['notes']
dt = parse_org_datetime(l['datetime']) dt = parse_org_datetime(l['datetime'])
try: try:
@ -75,9 +74,9 @@ def glucose_ketones_data() -> Iterable[Result]:
def blood_tests_data() -> Iterable[Result]: def blood_tests_data() -> Iterable[Result]:
o = porg.Org.from_file(str(config.blood_tests_log)) o = orgparse.load(config.blood_tests_log)
tbl = o.xpath('//table') tbl = one_table(o)
for d in tbl.lines: for d in tbl.as_dicts:
try: try:
dt = parse_org_datetime(d['datetime']) dt = parse_org_datetime(d['datetime'])
assert isinstance(dt, datetime), dt assert isinstance(dt, datetime), dt

View file

@ -9,6 +9,7 @@ from datetime import datetime, timedelta
from typing import Optional from typing import Optional
from ...core.pandas import DataFrameT, check_dataframe as cdf from ...core.pandas import DataFrameT, check_dataframe as cdf
from ...core.orgmode import collect, Table, parse_org_datetime, TypedTable
from my.config import exercise as config from my.config import exercise as config
@ -26,11 +27,15 @@ def tzify(d: datetime) -> datetime:
def cross_trainer_data(): def cross_trainer_data():
# FIXME some manual entries in python # FIXME some manual entries in python
# I guess just convert them to org # I guess just convert them to org
import orgparse
# todo should use all org notes and just query from them?
wlog = orgparse.load(config.workout_log)
from porg import Org [table] = collect(
# FIXME should use all org notes and just query from them? wlog,
wlog = Org.from_file(config.workout_log) lambda n: [] if n.heading != 'Cross training' else [x for x in n.body_rich if isinstance(x, Table)]
cross_table = wlog.xpath('//org[heading="Cross training"]//table') )
cross_table = TypedTable(table)
def maybe(f): def maybe(f):
def parse(s): def parse(s):
@ -46,13 +51,12 @@ def cross_trainer_data():
# todo eh. not sure if there is a way of getting around writing code... # todo eh. not sure if there is a way of getting around writing code...
# I guess would be nice to have a means of specifying type in the column? maybe multirow column names?? # I guess would be nice to have a means of specifying type in the column? maybe multirow column names??
# need to look up org-mode standard.. # need to look up org-mode standard..
from ...core.orgmode import parse_org_datetime
mappers = { mappers = {
'duration': lambda s: parse_mm_ss(s), 'duration': lambda s: parse_mm_ss(s),
'date' : lambda s: tzify(parse_org_datetime(s)), 'date' : lambda s: tzify(parse_org_datetime(s)),
'comment' : str, 'comment' : str,
} }
for row in cross_table.lines: for row in cross_table.as_dicts:
# todo make more defensive, fallback on nan for individual fields?? # todo make more defensive, fallback on nan for individual fields??
try: try:
d = {} d = {}

View file

@ -45,7 +45,7 @@ def from_orgmode() -> Iterator[Result]:
log.exception(e) log.exception(e)
yield e yield e
continue continue
# todo perhaps, better to use timezone provider # FIXME use timezone provider
created = config.default_timezone.localize(created) created = config.default_timezone.localize(created)
yield Entry( yield Entry(
dt=created, dt=created,

View file

@ -2,8 +2,6 @@
Various helpers for reading org-mode data Various helpers for reading org-mode data
""" """
from datetime import datetime from datetime import datetime
def parse_org_datetime(s: str) -> datetime: def parse_org_datetime(s: str) -> datetime:
s = s.strip('[]') s = s.strip('[]')
for fmt, cl in [ for fmt, cl in [
@ -19,3 +17,36 @@ def parse_org_datetime(s: str) -> datetime:
continue continue
else: else:
raise RuntimeError(f"Bad datetime string {s}") raise RuntimeError(f"Bad datetime string {s}")
from orgparse import OrgNode
from typing import Iterable, TypeVar, Callable
V = TypeVar('V')
def collect(n: OrgNode, cfun: Callable[[OrgNode], Iterable[V]]) -> Iterable[V]:
yield from cfun(n)
for c in n.children:
yield from collect(c, cfun)
from more_itertools import one
from orgparse.extra import Table
def one_table(o: OrgNode) -> Table:
return one(collect(o, lambda n: (x for x in n.body_rich if isinstance(x, Table))))
from typing import Iterator, Dict, Any
class TypedTable(Table):
def __new__(cls, orig: Table) -> 'TypedTable':
tt = super().__new__(TypedTable)
tt.__dict__ = orig.__dict__
blocks = list(orig.blocks)
header = blocks[0] # fist block is schema
if len(header) == 2:
# TODO later interpret first line as types
header = header[1:]
tt._blocks = [header, *blocks[1:]]
return tt
@property
def blocks(self):
return getattr(self, '_blocks')

View file

@ -5,14 +5,14 @@ from datetime import datetime, date
from pathlib import Path from pathlib import Path
from typing import List, Sequence, Iterable, NamedTuple, Optional from typing import List, Sequence, Iterable, NamedTuple, Optional
from .core import PathIsh from .core import PathIsh, get_files
from .core.common import mcachew from .core.common import mcachew
from .core.cachew import cache_dir from .core.cachew import cache_dir
from .core.orgmode import collect
from my.config import orgmode as user_config from my.config import orgmode as user_config
import orgparse
from porg import Org
# temporary? hack to cache org-mode notes # temporary? hack to cache org-mode notes
@ -28,12 +28,33 @@ def _sanitize(p: Path) -> str:
return re.sub(r'\W', '_', str(p)) return re.sub(r'\W', '_', str(p))
def to_note(x: Org) -> OrgNote: from typing import Tuple
_rgx = re.compile(orgparse.date.gene_timestamp_regex(brtype='inactive'), re.VERBOSE)
def _created(n: orgparse.OrgNode) -> Tuple[Optional[datetime], str]:
heading = n.heading
# meh.. support in orgparse?
pp = {} if n.is_root() else n.properties # type: ignore
createds = pp.get('CREATED', None)
if createds is None:
# try to guess from heading
m = _rgx.search(heading)
if m is not None:
createds = m.group(0) # could be None
if createds is None:
return (None, heading)
[odt] = orgparse.date.OrgDate.list_from_str(createds)
dt = odt.start
# todo a bit hacky..
heading = heading.replace(createds + ' ', '')
return (dt, heading)
def to_note(x: orgparse.OrgNode) -> OrgNote:
# ugh. hack to merely make it cacheable # ugh. hack to merely make it cacheable
heading = x.heading
created: Optional[datetime] created: Optional[datetime]
try: try:
# TODO(porg) not sure if created should ever throw... maybe warning/log? c, heading = _created(x)
c = x.created
if isinstance(c, datetime): if isinstance(c, datetime):
created = c created = c
else: else:
@ -43,12 +64,11 @@ def to_note(x: Org) -> OrgNote:
created = None created = None
return OrgNote( return OrgNote(
created=created, created=created,
heading=x.heading, # todo include the rest? heading=heading, # todo include the body?
tags=list(x.tags), tags=list(x.tags),
) )
# todo move to porg?
class Query: class Query:
def __init__(self, files: Sequence[Path]) -> None: def __init__(self, files: Sequence[Path]) -> None:
self.files = files self.files = files
@ -59,27 +79,20 @@ class Query:
depends_on=lambda _, f: (f, f.stat().st_mtime), depends_on=lambda _, f: (f, f.stat().st_mtime),
) )
def _iterate(self, f: Path) -> Iterable[OrgNote]: def _iterate(self, f: Path) -> Iterable[OrgNote]:
o = Org.from_file(f) o = orgparse.load(f)
for x in o.iterate(): for x in o:
yield to_note(x) yield to_note(x)
def all(self) -> Iterable[OrgNote]: def all(self) -> Iterable[OrgNote]:
# TODO build a virtual hierarchy from it?
for f in self.files: for f in self.files:
yield from self._iterate(f) yield from self._iterate(f)
# TODO very confusing names... def collect_all(self, collector) -> Iterable[orgparse.OrgNode]:
# TODO careful... maybe use orgparse iterate instead?? ugh. for f in self.files:
def get_all(self): o = orgparse.load(f)
return self._xpath_all('//org') yield from collect(o, collector)
def query_all(self, query):
for of in self.files:
org = Org.from_file(str(of))
yield from query(org)
def _xpath_all(self, query: str):
return self.query_all(lambda x: x.xpath_all(query))
def query() -> Query: def query() -> Query:
return Query(files=list(user_config.files)) return Query(files=get_files(user_config.paths))

View file

@ -6,9 +6,9 @@ def setup_notes_path(notes: Path) -> None:
from my.cfg import config from my.cfg import config
class user_config: class user_config:
roots = [notes] paths = [notes]
config.orgmode = user_config # type: ignore[misc,assignment] config.orgmode = user_config # type: ignore[misc,assignment]
# TODO FIXME ugh. this belongs to tz provider or global config or someting # TODO ugh. this belongs to tz provider or global config or someting
import pytz import pytz
class user_config_2: class user_config_2:
default_timezone = pytz.timezone('Europe/London') default_timezone = pytz.timezone('Europe/London')

View file

@ -1,7 +1,7 @@
from my import orgmode from my import orgmode
from my.core.orgmode import collect
def test() -> None: def test() -> None:
# meh # meh
results = list(orgmode.query().query_all(lambda x: x.with_tag('python'))) results = list(orgmode.query().collect_all(lambda n: [n] if 'python' in n.tags else []))
assert len(results) > 5 assert len(results) > 5

14
tox.ini
View file

@ -20,6 +20,9 @@ commands =
# my.calendar.holidays dep # my.calendar.holidays dep
pip install workalendar pip install workalendar
# my.body.weight dep
pip install orgparse
python3 -m pytest \ python3 -m pytest \
tests/core.py \ tests/core.py \
tests/misc.py \ tests/misc.py \
@ -29,10 +32,8 @@ commands =
tests/bluemaestro.py \ tests/bluemaestro.py \
tests/location.py \ tests/location.py \
tests/tz.py \ tests/tz.py \
tests/calendar.py tests/calendar.py \
# TODO add; once I figure out porg depdencency?? tests/config.py tests/config.py
# TODO run demo.py? just make sure with_my is a bit cleverer?
# TODO e.g. under CI, rely on installing
hpi modules hpi modules
@ -45,6 +46,7 @@ commands = ./demo.py
whitelist_externals = bash whitelist_externals = bash
commands = commands =
pip install -e .[testing] .[optional] pip install -e .[testing] .[optional]
pip install orgparse
pip install git+https://github.com/karlicoss/ghexport pip install git+https://github.com/karlicoss/ghexport
pip install git+https://github.com/karlicoss/hypexport pip install git+https://github.com/karlicoss/hypexport
pip install git+https://github.com/karlicoss/instapexport pip install git+https://github.com/karlicoss/instapexport
@ -52,8 +54,6 @@ commands =
pip install git+https://github.com/karlicoss/rexport pip install git+https://github.com/karlicoss/rexport
pip install git+https://github.com/karlicoss/endoexport pip install git+https://github.com/karlicoss/endoexport
pip install git+https://github.com/karlicoss/porg
# ugh fuck. soo... need to reset HOME, otherwise user's site-packages are somehow leaking into mypy's path... # ugh fuck. soo... need to reset HOME, otherwise user's site-packages are somehow leaking into mypy's path...
# see https://github.com/python/mypy/blob/f6fb60ef69738cbfe2dfe56c747eca8f03735d8e/mypy/modulefinder.py#L487 # see https://github.com/python/mypy/blob/f6fb60ef69738cbfe2dfe56c747eca8f03735d8e/mypy/modulefinder.py#L487
# this is particularly annoying when user's config is leaking and mypy isn't running against the repository config # this is particularly annoying when user's config is leaking and mypy isn't running against the repository config
@ -82,5 +82,5 @@ commands =
[testenv:mypy] [testenv:mypy]
skip_install = true skip_install = true
commands = commands =
pip install -e .[testing] .[optional] pip install -e .[testing] .[optional] orgparse
./lint ./lint