promnesia 1.3.20241021__py3-none-any.whl → 1.4.20250909__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. promnesia/__init__.py +4 -1
  2. promnesia/__main__.py +72 -59
  3. promnesia/cannon.py +90 -89
  4. promnesia/common.py +74 -62
  5. promnesia/compare.py +15 -10
  6. promnesia/config.py +22 -17
  7. promnesia/database/dump.py +1 -2
  8. promnesia/extract.py +6 -6
  9. promnesia/logging.py +27 -15
  10. promnesia/misc/install_server.py +25 -19
  11. promnesia/server.py +69 -53
  12. promnesia/sources/auto.py +65 -51
  13. promnesia/sources/browser.py +7 -2
  14. promnesia/sources/browser_legacy.py +51 -40
  15. promnesia/sources/demo.py +0 -1
  16. promnesia/sources/fbmessenger.py +0 -1
  17. promnesia/sources/filetypes.py +15 -11
  18. promnesia/sources/github.py +4 -1
  19. promnesia/sources/guess.py +4 -1
  20. promnesia/sources/hackernews.py +5 -7
  21. promnesia/sources/hpi.py +3 -1
  22. promnesia/sources/html.py +4 -2
  23. promnesia/sources/instapaper.py +1 -0
  24. promnesia/sources/markdown.py +4 -4
  25. promnesia/sources/org.py +17 -8
  26. promnesia/sources/plaintext.py +14 -11
  27. promnesia/sources/pocket.py +2 -1
  28. promnesia/sources/reddit.py +5 -8
  29. promnesia/sources/roamresearch.py +3 -1
  30. promnesia/sources/rss.py +4 -5
  31. promnesia/sources/shellcmd.py +3 -6
  32. promnesia/sources/signal.py +14 -14
  33. promnesia/sources/smscalls.py +0 -1
  34. promnesia/sources/stackexchange.py +2 -2
  35. promnesia/sources/takeout.py +14 -21
  36. promnesia/sources/takeout_legacy.py +16 -10
  37. promnesia/sources/telegram.py +7 -3
  38. promnesia/sources/telegram_legacy.py +5 -5
  39. promnesia/sources/twitter.py +1 -1
  40. promnesia/sources/vcs.py +6 -3
  41. promnesia/sources/viber.py +2 -2
  42. promnesia/sources/website.py +4 -3
  43. promnesia/sqlite.py +10 -7
  44. promnesia/tests/common.py +2 -0
  45. promnesia/tests/server_helper.py +2 -2
  46. promnesia/tests/sources/test_filetypes.py +9 -7
  47. promnesia/tests/sources/test_hypothesis.py +7 -3
  48. promnesia/tests/sources/test_org.py +7 -2
  49. promnesia/tests/sources/test_plaintext.py +9 -7
  50. promnesia/tests/sources/test_shellcmd.py +10 -9
  51. promnesia/tests/test_cannon.py +254 -237
  52. promnesia/tests/test_cli.py +8 -2
  53. promnesia/tests/test_compare.py +16 -12
  54. promnesia/tests/test_db_dump.py +4 -3
  55. promnesia/tests/test_extract.py +7 -4
  56. promnesia/tests/test_indexer.py +10 -10
  57. promnesia/tests/test_server.py +10 -10
  58. promnesia/tests/utils.py +1 -5
  59. promnesia-1.4.20250909.dist-info/METADATA +66 -0
  60. promnesia-1.4.20250909.dist-info/RECORD +80 -0
  61. {promnesia-1.3.20241021.dist-info → promnesia-1.4.20250909.dist-info}/WHEEL +1 -2
  62. promnesia/kjson.py +0 -122
  63. promnesia/sources/__init__.pyi +0 -0
  64. promnesia-1.3.20241021.dist-info/METADATA +0 -55
  65. promnesia-1.3.20241021.dist-info/RECORD +0 -83
  66. promnesia-1.3.20241021.dist-info/top_level.txt +0 -1
  67. {promnesia-1.3.20241021.dist-info → promnesia-1.4.20250909.dist-info}/entry_points.txt +0 -0
  68. {promnesia-1.3.20241021.dist-info → promnesia-1.4.20250909.dist-info/licenses}/LICENSE +0 -0
promnesia/__init__.py CHANGED
@@ -14,4 +14,7 @@ from .common import ( # noqa: F401
14
14
  )
15
15
 
16
16
  # TODO think again about it -- what are the pros and cons?
17
- warnings.warn("DEPRECATED! Please import directly from 'promnesia.common', e.g. 'from promnesia.common import Visit, Source, Results'", DeprecationWarning)
17
+ warnings.warn(
18
+ "DEPRECATED! Please import directly from 'promnesia.common', e.g. 'from promnesia.common import Visit, Source, Results'",
19
+ DeprecationWarning,
20
+ )
promnesia/__main__.py CHANGED
@@ -8,11 +8,10 @@ import os
8
8
  import shlex
9
9
  import shutil
10
10
  import sys
11
- from collections.abc import Iterable, Iterator, Sequence
11
+ from collections.abc import Callable, Iterable, Iterator, Sequence
12
12
  from pathlib import Path
13
13
  from subprocess import Popen, check_call, run
14
14
  from tempfile import TemporaryDirectory, gettempdir
15
- from typing import Callable
16
15
 
17
16
  from . import config, server
18
17
  from .common import (
@@ -54,7 +53,7 @@ def iter_all_visits(sources_subset: Iterable[str | int] = ()) -> Iterator[Res[Db
54
53
  if name and is_subset_sources:
55
54
  matched = name in sources_subset or i in sources_subset
56
55
  if matched:
57
- sources_subset -= {i, name} # type: ignore
56
+ sources_subset -= {i, name} # type: ignore[operator]
58
57
  else:
59
58
  logger.debug("skipping '%s' not in --sources.", name)
60
59
  continue
@@ -69,8 +68,7 @@ def iter_all_visits(sources_subset: Iterable[str | int] = ()) -> Iterator[Res[Db
69
68
  yield RuntimeError(f"Shouldn't have gotten this as a source: {source}")
70
69
  continue
71
70
 
72
- # todo hmm it's not even used??
73
- einfo = source.description
71
+ _einfo = source.description # FIXME hmm it's not even used?? add as exception notes?
74
72
  for v in extract_visits(source, src=source.name):
75
73
  if hook is None:
76
74
  yield v
@@ -80,13 +78,16 @@ def iter_all_visits(sources_subset: Iterable[str | int] = ()) -> Iterator[Res[Db
80
78
  except Exception as e:
81
79
  yield e
82
80
 
83
- if sources_subset:
81
+ if sources_subset: # type: ignore[truthy-iterable]
84
82
  logger.warning("unknown --sources: %s", ", ".join(repr(i) for i in sources_subset))
85
83
 
86
84
 
87
- def _do_index(*, dry: bool = False, sources_subset: Iterable[str | int] = (), overwrite_db: bool = False) -> Iterable[Exception]:
85
+ def _do_index(
86
+ *, dry: bool = False, sources_subset: Iterable[str | int] = (), overwrite_db: bool = False
87
+ ) -> Iterable[Exception]:
88
88
  # also keep & return errors for further display
89
89
  errors: list[Exception] = []
90
+
90
91
  def it() -> Iterable[Res[DbVisit]]:
91
92
  for v in iter_all_visits(sources_subset):
92
93
  if isinstance(v, Exception):
@@ -113,7 +114,7 @@ def do_index(
113
114
  sources_subset: Iterable[str | int] = (),
114
115
  overwrite_db: bool = False,
115
116
  ) -> Sequence[Exception]:
116
- config.load_from(config_file) # meh.. should be cleaner
117
+ config.load_from(config_file) # meh.. should be cleaner
117
118
  try:
118
119
  errors = list(_do_index(dry=dry, sources_subset=sources_subset, overwrite_db=overwrite_db))
119
120
  finally:
@@ -133,12 +134,15 @@ def demo_sources() -> dict[str, Callable[[], Extractor]]:
133
134
  def inner() -> Extractor:
134
135
  # TODO why this import??
135
136
  from . import sources # noqa: F401
137
+
136
138
  module = importlib.import_module(f'promnesia.sources.{name}')
137
139
  return getattr(module, 'index')
140
+
138
141
  return inner
139
142
 
140
143
  res = {}
141
144
  import promnesia.sources
145
+
142
146
  path: list[str] = getattr(promnesia.sources, '__path__') # should be present
143
147
  for p in path:
144
148
  for x in sorted(Path(p).glob('*.py')):
@@ -150,16 +154,16 @@ def demo_sources() -> dict[str, Callable[[], Extractor]]:
150
154
 
151
155
 
152
156
  def do_demo(
153
- *,
154
- index_as: str,
155
- params: Sequence[str],
156
- port: str | None,
157
- config_file: Path | None,
158
- dry: bool=False,
159
- name: str='demo',
160
- sources_subset: Iterable[str | int]=(),
161
- overwrite_db: bool=False,
162
- ) -> None:
157
+ *,
158
+ index_as: str,
159
+ params: Sequence[str],
160
+ port: str | None,
161
+ config_file: Path | None,
162
+ dry: bool = False,
163
+ name: str = 'demo',
164
+ sources_subset: Iterable[str | int] = (),
165
+ overwrite_db: bool = False,
166
+ ) -> None:
163
167
  with TemporaryDirectory() as tdir:
164
168
  outdir = Path(tdir)
165
169
 
@@ -182,17 +186,17 @@ def do_demo(
182
186
 
183
187
  dbp = config.get().db
184
188
  if port is None:
185
- logger.warning(f"Port isn't specified, not serving!\nYou can inspect the database in the meantime, e.g. 'sqlitebrowser {dbp}'")
189
+ logger.warning(
190
+ f"Port isn't specified, not serving!\nYou can inspect the database in the meantime, e.g. 'sqlitebrowser {dbp}'"
191
+ )
186
192
  else:
187
193
  from .server import ServerConfig
194
+
188
195
  server._run(
189
196
  host='127.0.0.1',
190
197
  port=port,
191
198
  quiet=False,
192
- config=ServerConfig(
193
- db=dbp,
194
- timezone=get_system_tz()
195
- ),
199
+ config=ServerConfig(db=dbp, timezone=get_system_tz()),
196
200
  )
197
201
 
198
202
  if sys.stdin.isatty():
@@ -201,6 +205,7 @@ def do_demo(
201
205
 
202
206
  def read_example_config() -> str:
203
207
  from .misc import config_example
208
+
204
209
  return inspect.getsource(config_example)
205
210
 
206
211
 
@@ -214,7 +219,10 @@ def config_create(args: argparse.Namespace) -> None:
214
219
  stub = read_example_config()
215
220
  cfgdir.mkdir(parents=True)
216
221
  cfg.write_text(stub)
217
- logger.info("Created a stub config in '%s'. Edit it to tune to your liking. (see https://github.com/karlicoss/promnesia#setup for more info)", cfg)
222
+ logger.info(
223
+ "Created a stub config in '%s'. Edit it to tune to your liking. (see https://github.com/karlicoss/promnesia#setup for more info)",
224
+ cfg,
225
+ )
218
226
 
219
227
 
220
228
  def config_check(args: argparse.Namespace) -> None:
@@ -245,7 +253,7 @@ def _config_check(cfg: Path) -> Iterable[Exception]:
245
253
  **os.environ,
246
254
  # if config is on read only partition, the command would fail due to generated bytecode
247
255
  # so put it in the temporary directory instead
248
- 'PYTHONPYCACHEPREFIX': gettempdir()
256
+ 'PYTHONPYCACHEPREFIX': gettempdir(),
249
257
  },
250
258
  )
251
259
 
@@ -256,16 +264,20 @@ def _config_check(cfg: Path) -> Iterable[Exception]:
256
264
  except ImportError:
257
265
  logger.warning("mypy not found, can't use it to check config!")
258
266
  else:
259
- yield from check([
260
- sys.executable, '-m', 'mypy',
261
- '--namespace-packages',
262
- '--color-output', # not sure if works??
263
- '--pretty',
264
- '--show-error-codes',
265
- '--show-error-context',
266
- '--check-untyped-defs',
267
- cfg,
268
- ])
267
+ yield from check(
268
+ [
269
+ sys.executable,
270
+ '-m',
271
+ 'mypy',
272
+ '--namespace-packages',
273
+ '--color-output', # not sure if works??
274
+ '--pretty',
275
+ '--show-error-codes',
276
+ '--show-error-context',
277
+ '--check-untyped-defs',
278
+ cfg,
279
+ ]
280
+ )
269
281
 
270
282
  logger.info('Checking runtime errors...')
271
283
  yield from check([sys.executable, cfg])
@@ -273,7 +285,7 @@ def _config_check(cfg: Path) -> Iterable[Exception]:
273
285
 
274
286
  def cli_doctor_db(args: argparse.Namespace) -> None:
275
287
  # todo could fallback to 'sqlite3 <db> .dump'?
276
- config.load_from(args.config) # TODO meh
288
+ config.load_from(args.config) # TODO meh
277
289
  db = config.get().db
278
290
  if not db.exists():
279
291
  logger.error("Database {db} doesn't exist!")
@@ -300,16 +312,15 @@ def cli_doctor_server(args: argparse.Namespace) -> None:
300
312
  cmd = ['curl', endpoint]
301
313
  logger.info(f'Running {cmd}')
302
314
  check_call(cmd)
303
- print() # curl doesn't add newline
315
+ print() # curl doesn't add newline
304
316
  logger.info('You should see the database path and version above!')
305
317
 
306
318
 
307
319
  def _ordinal_or_name(s: str) -> str | int:
308
320
  try:
309
- s = int(s) # type: ignore
321
+ return int(s)
310
322
  except ValueError:
311
- pass
312
- return s
323
+ return s
313
324
 
314
325
 
315
326
  def main() -> None:
@@ -321,7 +332,9 @@ def main() -> None:
321
332
  if not given, all :func:`demo_sources()` are run
322
333
  """
323
334
  parser.add_argument('--config', type=Path, default=default_config_path, help='Config path')
324
- parser.add_argument('--dry', action='store_true', help="Dry run, won't touch the database, only print the results out")
335
+ parser.add_argument(
336
+ '--dry', action='store_true', help="Dry run, won't touch the database, only print the results out"
337
+ )
325
338
  parser.add_argument(
326
339
  '--sources',
327
340
  required=False,
@@ -335,13 +348,12 @@ def main() -> None:
335
348
  '--overwrite',
336
349
  required=False,
337
350
  action="store_true",
338
- help="Empty db before populating it with newly indexed visits."
339
- " If interrupted, db is left untouched."
351
+ help="Empty db before populating it with newly indexed visits. If interrupted, db is left untouched.",
340
352
  )
341
353
 
342
354
  F = lambda prog: argparse.ArgumentDefaultsHelpFormatter(prog, width=120)
343
355
  p = argparse.ArgumentParser(formatter_class=F)
344
- subp = p.add_subparsers(dest='mode' )
356
+ subp = p.add_subparsers(dest='mode')
345
357
  ep = subp.add_parser('index', help='Create/update the link database', formatter_class=F)
346
358
  add_index_args(ep, default_config_path())
347
359
  # TODO use some way to override or provide config only via cmdline?
@@ -354,11 +366,13 @@ def main() -> None:
354
366
  # TODO use docstring or something?
355
367
  #
356
368
 
357
- add_port_arg = lambda p: p.add_argument('--port', type=str, default='13131' , help='Port to serve on')
369
+ add_port_arg = lambda p: p.add_argument('--port', type=str, default='13131', help='Port to serve on')
358
370
 
359
- ap.add_argument('--name', type=str, default='demo' , help='Set custom source name')
371
+ ap.add_argument('--name', type=str, default='demo', help='Set custom source name')
360
372
  add_port_arg(ap)
361
- ap.add_argument('--no-serve', action='store_const', const=None, dest='port', help='Pass to only index without running server')
373
+ ap.add_argument(
374
+ '--no-serve', action='store_const', const=None, dest='port', help='Pass to only index without running server'
375
+ )
362
376
  ap.add_argument(
363
377
  '--as',
364
378
  choices=sorted(demo_sources().keys()),
@@ -368,7 +382,9 @@ def main() -> None:
368
382
  add_index_args(ap)
369
383
  ap.add_argument('params', nargs='*', help='Optional extra params for the indexer')
370
384
 
371
- isp = subp.add_parser('install-server', help='Install server as a systemd service (for autostart)', formatter_class=F)
385
+ isp = subp.add_parser(
386
+ 'install-server', help='Install server as a systemd service (for autostart)', formatter_class=F
387
+ )
372
388
  install_server.setup_parser(isp)
373
389
 
374
390
  cp = subp.add_parser('config', help='Config management')
@@ -379,18 +395,16 @@ def main() -> None:
379
395
  ccp.add_argument('--config', type=Path, default=default_config_path(), help='Config path')
380
396
 
381
397
  icp = scp.add_parser('create', help='Create user config')
382
- icp.add_argument(
383
- "--config", type=Path, default=default_config_path(), help="Config path"
384
- )
398
+ icp.add_argument("--config", type=Path, default=default_config_path(), help="Config path")
385
399
  icp.set_defaults(func=config_create)
386
400
 
387
401
  dp = subp.add_parser('doctor', help='Troubleshooting assistant')
388
402
  dp.add_argument('--config', type=Path, default=default_config_path(), help='Config path')
389
403
  dp.set_defaults(func=lambda *_args: dp.print_help())
390
404
  sdp = dp.add_subparsers()
391
- sdp.add_parser('config' , help='Check config' ).set_defaults(func=config_check )
405
+ sdp.add_parser('config', help='Check config').set_defaults(func=config_check)
392
406
  sdp.add_parser('database', help='Inspect database').set_defaults(func=cli_doctor_db)
393
- sdps = sdp.add_parser('server' , help='Check server' )
407
+ sdps = sdp.add_parser('server', help='Check server')
394
408
  sdps.set_defaults(func=cli_doctor_server)
395
409
  add_port_arg(sdps)
396
410
 
@@ -409,7 +423,7 @@ def main() -> None:
409
423
  # the only downside is storage. dunno.
410
424
  # worst case -- could use database?
411
425
 
412
- with get_tmpdir() as tdir: # TODO??
426
+ with get_tmpdir() as _tdir: # TODO what's the tmp dir for??
413
427
  if mode == 'index':
414
428
  errors = do_index(
415
429
  config_file=args.config,
@@ -433,15 +447,14 @@ def main() -> None:
433
447
  name=args.name,
434
448
  sources_subset=args.sources,
435
449
  overwrite_db=args.overwrite,
436
- )
437
- elif mode == 'install-server': # todo rename to 'autostart' or something?
450
+ )
451
+ elif mode == 'install-server': # todo rename to 'autostart' or something?
438
452
  install_server.install(args)
439
- elif mode == 'config':
440
- args.func(args)
441
- elif mode == 'doctor':
453
+ elif mode == 'config' or mode == 'doctor':
442
454
  args.func(args)
443
455
  else:
444
456
  raise AssertionError(f'unexpected mode {mode}')
445
457
 
458
+
446
459
  if __name__ == '__main__':
447
460
  main()