lambda-watcher 0.1.0__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.
- lambda_watcher/__init__.py +4 -0
- lambda_watcher/__main__.py +4 -0
- lambda_watcher/analysis/__init__.py +115 -0
- lambda_watcher/analysis/deps.py +291 -0
- lambda_watcher/analysis/envvars.py +80 -0
- lambda_watcher/analysis/handler.py +111 -0
- lambda_watcher/analysis/inventory.py +118 -0
- lambda_watcher/analysis/runtime.py +117 -0
- lambda_watcher/analysis/secrets.py +178 -0
- lambda_watcher/analysis/services.py +76 -0
- lambda_watcher/cli.py +1406 -0
- lambda_watcher/config.py +324 -0
- lambda_watcher/db.py +466 -0
- lambda_watcher/diffing/__init__.py +14 -0
- lambda_watcher/diffing/build.py +51 -0
- lambda_watcher/diffing/compare.py +525 -0
- lambda_watcher/diffing/highlight.py +312 -0
- lambda_watcher/diffing/icons.py +132 -0
- lambda_watcher/diffing/intraline.py +162 -0
- lambda_watcher/diffing/render_html.py +697 -0
- lambda_watcher/diffing/render_text.py +198 -0
- lambda_watcher/extract.py +227 -0
- lambda_watcher/gitmirror.py +151 -0
- lambda_watcher/identify.py +201 -0
- lambda_watcher/ingest.py +480 -0
- lambda_watcher/notify.py +59 -0
- lambda_watcher/reindex.py +158 -0
- lambda_watcher/service.py +553 -0
- lambda_watcher/store.py +209 -0
- lambda_watcher/templates.py +124 -0
- lambda_watcher/utils.py +314 -0
- lambda_watcher/watcher.py +241 -0
- lambda_watcher-0.1.0.dist-info/METADATA +409 -0
- lambda_watcher-0.1.0.dist-info/RECORD +38 -0
- lambda_watcher-0.1.0.dist-info/WHEEL +5 -0
- lambda_watcher-0.1.0.dist-info/entry_points.txt +3 -0
- lambda_watcher-0.1.0.dist-info/licenses/LICENSE +201 -0
- lambda_watcher-0.1.0.dist-info/top_level.txt +1 -0
lambda_watcher/notify.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Best-effort desktop notifications, with no extra dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .utils import LOG
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def notify(title: str, message: str, enabled: bool = True) -> bool:
|
|
13
|
+
"""Show a desktop notification. Never raises; returns True if it fired."""
|
|
14
|
+
if not enabled:
|
|
15
|
+
return False
|
|
16
|
+
try:
|
|
17
|
+
if sys.platform == "darwin":
|
|
18
|
+
script = (
|
|
19
|
+
f'display notification {_applescript_quote(message)} '
|
|
20
|
+
f'with title {_applescript_quote(title)}'
|
|
21
|
+
)
|
|
22
|
+
subprocess.run(["osascript", "-e", script], capture_output=True, timeout=10)
|
|
23
|
+
return True
|
|
24
|
+
if sys.platform.startswith("win"):
|
|
25
|
+
ps = (
|
|
26
|
+
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications,"
|
|
27
|
+
" ContentType = WindowsRuntime] > $null; "
|
|
28
|
+
"$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent(2); "
|
|
29
|
+
f"$t.GetElementsByTagName('text')[0]"
|
|
30
|
+
f".AppendChild($t.CreateTextNode({_ps_quote(title)})) > $null; "
|
|
31
|
+
f"$t.GetElementsByTagName('text')[1]"
|
|
32
|
+
f".AppendChild($t.CreateTextNode({_ps_quote(message)})) > $null; "
|
|
33
|
+
"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('lambda-watcher')"
|
|
34
|
+
".Show([Windows.UI.Notifications.ToastNotification]::new($t))"
|
|
35
|
+
)
|
|
36
|
+
subprocess.run(
|
|
37
|
+
["powershell", "-NoProfile", "-NonInteractive", "-Command", ps],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
timeout=15,
|
|
40
|
+
)
|
|
41
|
+
return True
|
|
42
|
+
if shutil.which("notify-send"):
|
|
43
|
+
subprocess.run(
|
|
44
|
+
["notify-send", "-a", "lambda-watcher", title, message],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
timeout=10,
|
|
47
|
+
)
|
|
48
|
+
return True
|
|
49
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
50
|
+
LOG.debug("notification failed: %s", exc)
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _applescript_quote(value: str) -> str:
|
|
55
|
+
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ps_quote(value: str) -> str:
|
|
59
|
+
return "'" + value.replace("'", "''") + "'"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Rebuild the SQLite index from the manifests on disk.
|
|
2
|
+
|
|
3
|
+
The archive directories are the source of truth, so the index can always be
|
|
4
|
+
thrown away and reconstructed — after a crash, a manual reorganisation, or a
|
|
5
|
+
copy of the store onto another machine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .config import Config
|
|
14
|
+
from .db import Database
|
|
15
|
+
from .store import Store
|
|
16
|
+
from .utils import LOG, utc_now_iso
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _iter_version_dirs(functions_dir: Path):
|
|
20
|
+
for function_dir in sorted(p for p in functions_dir.glob("*") if p.is_dir()):
|
|
21
|
+
versions = function_dir / "versions"
|
|
22
|
+
if not versions.is_dir():
|
|
23
|
+
continue
|
|
24
|
+
for version_dir in sorted(p for p in versions.glob("*") if p.is_dir()):
|
|
25
|
+
yield function_dir, version_dir
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def rebuild(cfg: Config) -> dict[str, int]:
|
|
29
|
+
"""Drop and repopulate the index. Returns counts for reporting."""
|
|
30
|
+
store = Store(cfg)
|
|
31
|
+
if cfg.db_path.exists():
|
|
32
|
+
backup = cfg.db_path.with_suffix(".db.bak")
|
|
33
|
+
try:
|
|
34
|
+
backup.unlink(missing_ok=True)
|
|
35
|
+
cfg.db_path.replace(backup)
|
|
36
|
+
except OSError as exc:
|
|
37
|
+
LOG.warning("could not back up the old index: %s", exc)
|
|
38
|
+
for suffix in ("-wal", "-shm"):
|
|
39
|
+
Path(str(cfg.db_path) + suffix).unlink(missing_ok=True)
|
|
40
|
+
|
|
41
|
+
db = Database(cfg.db_path)
|
|
42
|
+
stats = {"functions": 0, "versions": 0, "skipped": 0}
|
|
43
|
+
now = utc_now_iso()
|
|
44
|
+
seen_functions: dict[str, int] = {}
|
|
45
|
+
|
|
46
|
+
for function_dir, version_dir in _iter_version_dirs(cfg.functions_dir):
|
|
47
|
+
manifest = store.read_manifest(version_dir)
|
|
48
|
+
if not manifest:
|
|
49
|
+
LOG.warning("no manifest in %s, skipping", version_dir)
|
|
50
|
+
stats["skipped"] += 1
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
function = manifest.get("function") or {}
|
|
54
|
+
name = function.get("name") or function_dir.name
|
|
55
|
+
slug = function.get("slug") or function_dir.name
|
|
56
|
+
ingested_at = (manifest.get("version") or {}).get("ingested_at") or now
|
|
57
|
+
|
|
58
|
+
if name not in seen_functions:
|
|
59
|
+
seen_functions[name] = db.upsert_function(name, slug, ingested_at)
|
|
60
|
+
stats["functions"] += 1
|
|
61
|
+
function_id = seen_functions[name]
|
|
62
|
+
db.conn.execute(
|
|
63
|
+
"UPDATE functions SET last_seen = MAX(last_seen, ?) WHERE id = ?",
|
|
64
|
+
(ingested_at, function_id),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
_insert(db, store, function_id, manifest, version_dir)
|
|
69
|
+
stats["versions"] += 1
|
|
70
|
+
except Exception as exc: # noqa: BLE001
|
|
71
|
+
LOG.warning("could not index %s: %s", version_dir, exc)
|
|
72
|
+
stats["skipped"] += 1
|
|
73
|
+
|
|
74
|
+
db.log_event("reindex", now, detail=stats)
|
|
75
|
+
db.close()
|
|
76
|
+
return stats
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _insert(db: Database, store: Store, function_id: int, manifest: dict[str, Any],
|
|
80
|
+
version_dir: Path) -> None:
|
|
81
|
+
version_meta = manifest.get("version") or {}
|
|
82
|
+
source = manifest.get("source") or {}
|
|
83
|
+
runtime = manifest.get("runtime") or {}
|
|
84
|
+
totals = manifest.get("totals") or {}
|
|
85
|
+
handlers = manifest.get("handlers") or []
|
|
86
|
+
|
|
87
|
+
seq = int(version_meta.get("seq") or 0)
|
|
88
|
+
if not seq:
|
|
89
|
+
# Fall back to the numeric prefix of the directory name.
|
|
90
|
+
seq = int(version_dir.name.split("-")[0])
|
|
91
|
+
|
|
92
|
+
with db.transaction():
|
|
93
|
+
version_id = db.insert_version(
|
|
94
|
+
{
|
|
95
|
+
"function_id": function_id,
|
|
96
|
+
"seq": seq,
|
|
97
|
+
"tree_hash": manifest.get("tree_hash") or version_dir.name,
|
|
98
|
+
"zip_sha256": source.get("zip_sha256"),
|
|
99
|
+
"zip_size": source.get("zip_size"),
|
|
100
|
+
"source_name": source.get("filename"),
|
|
101
|
+
"source_path": source.get("path"),
|
|
102
|
+
"source_mtime": source.get("mtime"),
|
|
103
|
+
"ingested_at": version_meta.get("ingested_at") or utc_now_iso(),
|
|
104
|
+
"dir": store.relative(version_dir),
|
|
105
|
+
"runtime": runtime.get("runtime"),
|
|
106
|
+
"runtime_confidence": runtime.get("confidence"),
|
|
107
|
+
"handler": handlers[0]["handler"] if handlers else None,
|
|
108
|
+
"file_count": totals.get("file_count", 0),
|
|
109
|
+
"total_size": totals.get("total_size", 0),
|
|
110
|
+
"code_file_count": totals.get("code_file_count", 0),
|
|
111
|
+
"code_size": totals.get("code_size", 0),
|
|
112
|
+
"code_lines": totals.get("code_lines", 0),
|
|
113
|
+
"label": version_meta.get("label"),
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
db.bulk_insert(
|
|
117
|
+
"files",
|
|
118
|
+
["version_id", "path", "size", "sha256", "mode", "is_text", "is_vendor", "lang", "lines"],
|
|
119
|
+
[
|
|
120
|
+
(version_id, f["path"], f["size"], f["sha256"], f.get("mode"),
|
|
121
|
+
int(bool(f.get("is_text"))), int(bool(f.get("is_vendor"))),
|
|
122
|
+
f.get("lang"), f.get("lines", 0))
|
|
123
|
+
for f in manifest.get("files", [])
|
|
124
|
+
],
|
|
125
|
+
)
|
|
126
|
+
db.bulk_insert(
|
|
127
|
+
"deps", ["version_id", "manager", "name", "version", "source", "is_declared"],
|
|
128
|
+
[
|
|
129
|
+
(version_id, d["manager"], d["name"], d.get("version"), d.get("source"),
|
|
130
|
+
int(bool(d.get("is_declared"))))
|
|
131
|
+
for d in manifest.get("dependencies", [])
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
db.bulk_insert(
|
|
135
|
+
"env_vars", ["version_id", "name", "path", "line"],
|
|
136
|
+
[
|
|
137
|
+
(version_id, e["name"], e.get("path"), e.get("line"))
|
|
138
|
+
for e in manifest.get("env_vars", []) if not e.get("is_reserved")
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
db.bulk_insert(
|
|
142
|
+
"services", ["version_id", "service", "path", "line"],
|
|
143
|
+
[(version_id, s["service"], s.get("path"), s.get("line"))
|
|
144
|
+
for s in manifest.get("services", [])],
|
|
145
|
+
)
|
|
146
|
+
db.bulk_insert(
|
|
147
|
+
"findings", ["version_id", "kind", "severity", "path", "line", "detail", "is_vendor"],
|
|
148
|
+
[
|
|
149
|
+
(version_id, f["kind"], f["severity"], f.get("path"), f.get("line"),
|
|
150
|
+
f.get("detail"), int(bool(f.get("is_vendor"))))
|
|
151
|
+
for f in manifest.get("findings", [])
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
if source.get("zip_sha256"):
|
|
155
|
+
db.mark_download_seen(
|
|
156
|
+
source["zip_sha256"], version_meta.get("ingested_at") or utc_now_iso(),
|
|
157
|
+
source.get("filename") or "",
|
|
158
|
+
)
|