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.
- promnesia/__init__.py +4 -1
- promnesia/__main__.py +72 -59
- promnesia/cannon.py +90 -89
- promnesia/common.py +74 -62
- promnesia/compare.py +15 -10
- promnesia/config.py +22 -17
- promnesia/database/dump.py +1 -2
- promnesia/extract.py +6 -6
- promnesia/logging.py +27 -15
- promnesia/misc/install_server.py +25 -19
- promnesia/server.py +69 -53
- promnesia/sources/auto.py +65 -51
- promnesia/sources/browser.py +7 -2
- promnesia/sources/browser_legacy.py +51 -40
- promnesia/sources/demo.py +0 -1
- promnesia/sources/fbmessenger.py +0 -1
- promnesia/sources/filetypes.py +15 -11
- promnesia/sources/github.py +4 -1
- promnesia/sources/guess.py +4 -1
- promnesia/sources/hackernews.py +5 -7
- promnesia/sources/hpi.py +3 -1
- promnesia/sources/html.py +4 -2
- promnesia/sources/instapaper.py +1 -0
- promnesia/sources/markdown.py +4 -4
- promnesia/sources/org.py +17 -8
- promnesia/sources/plaintext.py +14 -11
- promnesia/sources/pocket.py +2 -1
- promnesia/sources/reddit.py +5 -8
- promnesia/sources/roamresearch.py +3 -1
- promnesia/sources/rss.py +4 -5
- promnesia/sources/shellcmd.py +3 -6
- promnesia/sources/signal.py +14 -14
- promnesia/sources/smscalls.py +0 -1
- promnesia/sources/stackexchange.py +2 -2
- promnesia/sources/takeout.py +14 -21
- promnesia/sources/takeout_legacy.py +16 -10
- promnesia/sources/telegram.py +7 -3
- promnesia/sources/telegram_legacy.py +5 -5
- promnesia/sources/twitter.py +1 -1
- promnesia/sources/vcs.py +6 -3
- promnesia/sources/viber.py +2 -2
- promnesia/sources/website.py +4 -3
- promnesia/sqlite.py +10 -7
- promnesia/tests/common.py +2 -0
- promnesia/tests/server_helper.py +2 -2
- promnesia/tests/sources/test_filetypes.py +9 -7
- promnesia/tests/sources/test_hypothesis.py +7 -3
- promnesia/tests/sources/test_org.py +7 -2
- promnesia/tests/sources/test_plaintext.py +9 -7
- promnesia/tests/sources/test_shellcmd.py +10 -9
- promnesia/tests/test_cannon.py +254 -237
- promnesia/tests/test_cli.py +8 -2
- promnesia/tests/test_compare.py +16 -12
- promnesia/tests/test_db_dump.py +4 -3
- promnesia/tests/test_extract.py +7 -4
- promnesia/tests/test_indexer.py +10 -10
- promnesia/tests/test_server.py +10 -10
- promnesia/tests/utils.py +1 -5
- promnesia-1.4.20250909.dist-info/METADATA +66 -0
- promnesia-1.4.20250909.dist-info/RECORD +80 -0
- {promnesia-1.3.20241021.dist-info → promnesia-1.4.20250909.dist-info}/WHEEL +1 -2
- promnesia/kjson.py +0 -122
- promnesia/sources/__init__.pyi +0 -0
- promnesia-1.3.20241021.dist-info/METADATA +0 -55
- promnesia-1.3.20241021.dist-info/RECORD +0 -83
- promnesia-1.3.20241021.dist-info/top_level.txt +0 -1
- {promnesia-1.3.20241021.dist-info → promnesia-1.4.20250909.dist-info}/entry_points.txt +0 -0
- {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(
|
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
|
-
#
|
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(
|
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)
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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(
|
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(
|
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
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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)
|
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()
|
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
|
-
|
321
|
+
return int(s)
|
310
322
|
except ValueError:
|
311
|
-
|
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(
|
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'
|
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'
|
371
|
+
ap.add_argument('--name', type=str, default='demo', help='Set custom source name')
|
360
372
|
add_port_arg(ap)
|
361
|
-
ap.add_argument(
|
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(
|
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'
|
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'
|
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
|
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':
|
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()
|