Skip to content

Commit

Permalink
general: use Exception.add_note instead of hacky my.core.error.echain
Browse files Browse the repository at this point in the history
before it was dumping a sequence of somewhat confusing exception messages, now it's printing something like this

```
[ERROR   2025-02-04 21:53:13,891 my.instagram.android run.py:9   ] ('<message_id>', "xma_link isn't handled yet")
Traceback (most recent call last):
  File "src/my/instagram/android.py", line 169, in _process_db
    m = _parse_message(j)
        ^^^^^^^^^^^^^^^^^
  File "/code/hpi/src/my/instagram/android.py", line 120, in _parse_message
    raise MessageError(id, f"{t} isn't handled yet")
my.instagram.android.MessageError: ('2932982394892980393664', "xma_link isn't handled yet")
^ while parsing {'status': 'UPLOADED', 'item_type': 'xma_link',...
^ while processing /path/to/db/direct.db
```

for python < 3.11, a backport is used

For more info, see:
- https://peps.python.org/pep-0678
- https://docs.python.org/3/tutorial/errors.html#tut-exception-notes
  • Loading branch information
karlicoss committed Feb 4, 2025
1 parent 8312602 commit acb4f6b
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 66 deletions.
27 changes: 27 additions & 0 deletions src/my/core/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,30 @@ def test_fromisoformat() -> None:
from typing import Never, assert_never, assert_type
else:
from typing_extensions import Never, assert_never, assert_type


if sys.version_info[:2] >= (3, 11):
add_note = BaseException.add_note
else:

def add_note(e: BaseException, note: str) -> None:
"""
Backport of BaseException.add_note
"""

# The only (somewhat annoying) difference is it will log extra lines for notes past the main exception message:
# (i.e. line 2 here:)

# 1 [ERROR 2025-02-04 22:12:21] Main exception message
# 2 ^ extra note
# 3 Traceback (most recent call last):
# 4 File "run.py", line 19, in <module>
# 5 ee = test()
# 6 File "run.py", line 5, in test
# 7 raise RuntimeError("Main exception message")
# 8 RuntimeError: Main exception message
# 9 ^ extra note

args = e.args
if len(args) == 1 and isinstance(args[0], str):
e.args = (e.args[0] + '\n' + note,)
1 change: 1 addition & 0 deletions src/my/core/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def _warn_func(e: Exception) -> None:
yield o


# TODO deprecate in favor of Exception.add_note?
def echain(ex: E, cause: Exception) -> E:
ex.__cause__ = cause
return ex
Expand Down
16 changes: 7 additions & 9 deletions src/my/fbmessenger/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@
from pathlib import Path
from typing import Union

from my.core import LazyLogger, Paths, Res, datetime_aware, get_files, make_config
from my.core import Paths, Res, datetime_aware, get_files, make_config, make_logger
from my.core.common import unique_everseen
from my.core.compat import assert_never
from my.core.error import echain
from my.core.compat import add_note, assert_never
from my.core.sqlite import SqliteTool, sqlite_connection

from my.config import fbmessenger as user_config # isort: skip


logger = LazyLogger(__name__)
logger = make_logger(__name__)


@dataclass
Expand Down Expand Up @@ -93,7 +92,8 @@ def _entities() -> Iterator[Res[Entity]]:
else:
yield from _process_db_threads_db2(db)
except Exception as e:
yield echain(RuntimeError(f'While processing {path}'), cause=e)
add_note(e, f'^ while processing {path}')
yield e


def _normalise_user_id(ukey: str) -> str:
Expand Down Expand Up @@ -260,10 +260,7 @@ def _process_db_threads_db2(db: sqlite3.Connection) -> Iterator[Res[Entity]]:

def contacts() -> Iterator[Res[Sender]]:
for x in unique_everseen(_entities):
if isinstance(x, Exception):
yield x
continue
if isinstance(x, Sender):
if isinstance(x, (Sender, Exception)):
yield x


Expand Down Expand Up @@ -291,6 +288,7 @@ def messages() -> Iterator[Res[Message]]:
sender = senders[x.sender_id]
thread = threads[x.thread_id]
except Exception as e:
add_note(e, f'^ while processing {x}')
yield e
continue
m = Message(
Expand Down
5 changes: 3 additions & 2 deletions src/my/github/gdpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import Any

from my.core import Paths, Res, Stats, get_files, make_logger, stat, warnings
from my.core.error import echain
from my.core.compat import add_note

from .common import Event, EventIds, parse_dt

Expand Down Expand Up @@ -127,7 +127,8 @@ def _process_one(root: Path) -> Iterator[Res[Event]]:
try:
yield handler(r)
except Exception as e:
yield echain(RuntimeError(f'While processing file: {f}'), e)
add_note(e, f'^ while processing {f}')
yield e


def stats() -> Stats:
Expand Down
17 changes: 11 additions & 6 deletions src/my/instagram/android.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Bumble data from Android app database (in =/data/data/com.instagram.android/databases/direct.db=)
"""

from __future__ import annotations

import json
Expand All @@ -14,15 +15,14 @@
Json,
Paths,
Res,
assert_never,
datetime_naive,
get_files,
make_config,
make_logger,
)
from my.core.cachew import mcachew
from my.core.common import unique_everseen
from my.core.error import echain
from my.core.compat import add_note, assert_never
from my.core.sqlite import select, sqlite_connect_immutable

from my.config import instagram as user_config # isort: skip
Expand Down Expand Up @@ -79,7 +79,7 @@ class Message(_BaseMessage):
# reply_to: Optional[Message]


# this is kinda expecrimental
# this is kinda experimental
# basically just using RuntimeError(msg_id, *rest) has an unfortunate consequence:
# there are way too many 'similar' errors (on different msg_id)
# however passing msg_id is nice as a means of supplying extra context
Expand All @@ -105,7 +105,7 @@ def _parse_message(j: Json) -> _Message | None:
t = j['item_type']
tid = j['thread_key']['thread_id']
uid = j['user_id']
created = datetime.fromtimestamp(int(j['timestamp']) / 1_000_000)
created: datetime_naive = datetime.fromtimestamp(int(j['timestamp']) / 1_000_000)
text: str | None = None
if t == 'text':
text = j['text']
Expand Down Expand Up @@ -171,6 +171,7 @@ def _process_db(db: sqlite3.Connection) -> Iterator[Res[User | _Message]]:
if m is not None:
yield m
except Exception as e:
add_note(e, f'^ while parsing {j}')
yield e


Expand All @@ -185,10 +186,14 @@ def _entities() -> Iterator[Res[User | _Message]]:
logger.info(f'processing [{idx:>{width}}/{total:>{width}}] {path}')
with sqlite_connect_immutable(path) as db:
try:
yield from _process_db(db=db)
for m in _process_db(db=db):
if isinstance(m, Exception):
add_note(m, f'^ while processing {path}')
yield m
except Exception as e:
add_note(e, f'^ while processing {path}')
yield e
# todo use error policy here
yield echain(RuntimeError(f'While processing {path}'), cause=e)


@mcachew(depends_on=inputs)
Expand Down
2 changes: 1 addition & 1 deletion src/my/photos/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _make_photo(photo: Path, mtype: str, *, parent_geo: LatLon | None) -> Iterat
try:
exif = get_exif_from_file(photo)
except Exception as e:
# TODO reuse echain from promnesia
# TODO add exception note?
yield e
exif = {}

Expand Down
88 changes: 46 additions & 42 deletions src/my/polar.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
"""
[[https://github.com/burtonator/polar-bookshelf][Polar]] articles and highlights
"""

from __future__ import annotations

import json
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, NamedTuple, cast

from my.core import (
Json,
PathIsh,
Res,
datetime_aware,
get_files,
make_config,
make_logger,
)
from my.core.compat import add_note, fromisoformat
from my.core.error import sort_res_by
from my.core.konsume import Wdict, Zoomable, wrap

import my.config # isort: skip

logger = make_logger(__name__)


# todo use something similar to tz.via_location for config fallback
if not TYPE_CHECKING:
user_config = getattr(my.config, 'polar', None)
Expand All @@ -17,55 +37,43 @@

# by default, Polar doesn't need any config, so perhaps makes sense to make it defensive here
if user_config is None:
class user_config: # type: ignore[no-redef]
pass


from dataclasses import dataclass

from .core import PathIsh
class user_config: # type: ignore[no-redef]
pass


@dataclass
class polar(user_config):
'''
Polar config is optional, you only need it if you want to specify custom 'polar_dir'
'''
polar_dir: PathIsh = Path('~/.polar').expanduser() # noqa: RUF009
defensive: bool = True # pass False if you want it to fail faster on errors (useful for debugging)

polar_dir: PathIsh = Path('~/.polar').expanduser() # noqa: RUF009
defensive: bool = True # pass False if you want it to fail faster on errors (useful for debugging)

from .core import make_config

config = make_config(polar)

# todo not sure where it keeps stuff on Windows?
# https://github.com/burtonator/polar-bookshelf/issues/296

import json
from collections.abc import Iterable, Sequence
from datetime import datetime
from typing import NamedTuple

from .core import Json, LazyLogger, Res
from .core.compat import fromisoformat
from .core.error import echain, sort_res_by
from .core.konsume import Wdict, Zoomable, wrap

logger = LazyLogger(__name__)


# Ok I guess handling comment-level errors is a bit too much..
Cid = str


class Comment(NamedTuple):
cid: Cid
created: datetime
created: datetime_aware
text: str


Hid = str


class Highlight(NamedTuple):
hid: Hid
created: datetime
created: datetime_aware
selection: str
comments: Sequence[Comment]
tags: Sequence[str]
Expand All @@ -74,8 +82,10 @@ class Highlight(NamedTuple):


Uid = str


class Book(NamedTuple):
created: datetime
created: datetime_aware
uid: Uid
path: Path
title: str | None
Expand All @@ -90,26 +100,22 @@ def filename(self) -> str:
# TODO deprecate
return str(self.path)


Result = Res[Book]


class Loader:
def __init__(self, p: Path) -> None:
self.path = p
self.uid = self.path.parent.name

def error(self, cause: Exception, extra: str ='') -> Exception:
if len(extra) > 0:
extra = '\n' + extra
return echain(Exception(f'while processing {self.path}{extra}'), cause)

def load_item(self, meta: Zoomable) -> Iterable[Highlight]:
meta = cast(Wdict, meta)
# TODO this should be destructive zoom?
meta['notes'].zoom() # TODO ??? is it deliberate?
meta['notes'].zoom() # TODO ??? is it deliberate?

meta['pagemarks'].consume_all()


if 'notes' in meta:
# TODO something nicer?
_notes = meta['notes'].zoom()
Expand Down Expand Up @@ -150,11 +156,12 @@ def load_item(self, meta: Zoomable) -> Iterable[Highlight]:
[_, hlid] = refv.split(':')
ccs = cmap.get(hlid, [])
cmap[hlid] = ccs
ccs.append(Comment(
comment = Comment(
cid=cid.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?
)
ccs.append(comment)
v.consume()
for h in list(highlights.values()):
hid = h['id'].zoom().value
Expand Down Expand Up @@ -204,7 +211,6 @@ def load_item(self, meta: Zoomable) -> Iterable[Highlight]:
# raise RuntimeError(f'Unconsumed comments: {cmap}')
# TODO sort by date?


def load_items(self, metas: Json) -> Iterable[Highlight]:
for _p, meta in metas.items(): # noqa: PERF102
with wrap(meta, throw=not config.defensive) as meta:
Expand All @@ -220,7 +226,7 @@ def load(self) -> Iterable[Result]:
filename = di['filename']
title = di.get('title', None)
tags_dict = di['tags']
pm = j['pageMetas'] # todo handle this too?
pm = j['pageMetas'] # todo handle this too?

# todo defensive?
tags = tuple(t['label'] for t in tags_dict.values())
Expand All @@ -238,15 +244,13 @@ def load(self) -> Iterable[Result]:


def iter_entries() -> Iterable[Result]:
from .core import get_files
for d in get_files(config.polar_dir, glob='*/state.json'):
loader = Loader(d)
try:
yield from loader.load()
except Exception as ee:
err = loader.error(ee)
logger.exception(err)
yield err
except Exception as e:
add_note(e, f'^ while processing {d}')
yield e


def get_entries() -> list[Result]:
Expand Down
Loading

0 comments on commit acb4f6b

Please sign in to comment.