Navegador

fix: remove silent fallbacks — errors now propagate naturally - planopticon _load_json: remove FileNotFoundError/JSONDecodeError swallowing; callers now see real errors instead of silent empty-dict results - planopticon _ingest_kg_relationship: DEBUG -> WARNING so edge failures are visible at default log level - planopticon _lazy_wiki_link: replace bare 'pass' with debug log - wiki _ingest_wiki_link: catch only ValueError (expected unknown label), let real store errors propagate to the caller - wiki ingest_github_api: DEBUG -> WARNING for per-file skip logging - Add pytest-asyncio to dev dependencies - Update tests to assert on the new failure-propagating behaviour

lmata 2026-03-23 00:32 trunk
Commit 7e708ec3514631d451299e38729bfadc05b04d4979e0c1163f6a35327eec6f19
--- navegador/ingestion/planopticon.py
+++ navegador/ingestion/planopticon.py
@@ -349,11 +349,11 @@
349349
"""
350350
try:
351351
self.store.query(cypher, {"src": src, "tgt": tgt})
352352
self._stats["edges"] = self._stats.get("edges", 0) + 1
353353
except Exception:
354
- logger.debug("Could not create edge %s -[%s]-> %s", src, edge_type, tgt)
354
+ logger.warning("Could not create edge %s -[%s]-> %s", src, edge_type, tgt)
355355
356356
def _ingest_key_points(self, key_points: list[dict], source: str) -> None:
357357
for kp in key_points:
358358
point = (kp.get("point") or "").strip()
359359
if not point:
@@ -472,24 +472,17 @@
472472
NodeLabel.WikiPage, {"name": source_id},
473473
EdgeType.DOCUMENTS,
474474
label, {"name": name},
475475
)
476476
except Exception:
477
- pass
477
+ logger.debug("Could not link %s to wiki page %s", name, source_id)
478478
479479
def _load_json(self, path: Path) -> Any:
480
- try:
481
- return json.loads(Path(path).read_text(encoding="utf-8"))
482
- except FileNotFoundError:
483
- logger.warning("File not found: %s", path)
484
- return {}
485
- except json.JSONDecodeError as e:
486
- logger.warning("Invalid JSON at %s: %s", path, e)
487
- return {}
480
+ return json.loads(Path(path).read_text(encoding="utf-8"))
488481
489482
def _reset_stats(self) -> dict[str, int]:
490483
self._stats = {"nodes": 0, "edges": 0}
491484
return self._stats
492485
493486
def _merge_stats(self, other: dict[str, int]) -> None:
494487
for k, v in other.items():
495488
self._stats[k] = self._stats.get(k, 0) + v
496489
--- navegador/ingestion/planopticon.py
+++ navegador/ingestion/planopticon.py
@@ -349,11 +349,11 @@
349 """
350 try:
351 self.store.query(cypher, {"src": src, "tgt": tgt})
352 self._stats["edges"] = self._stats.get("edges", 0) + 1
353 except Exception:
354 logger.debug("Could not create edge %s -[%s]-> %s", src, edge_type, tgt)
355
356 def _ingest_key_points(self, key_points: list[dict], source: str) -> None:
357 for kp in key_points:
358 point = (kp.get("point") or "").strip()
359 if not point:
@@ -472,24 +472,17 @@
472 NodeLabel.WikiPage, {"name": source_id},
473 EdgeType.DOCUMENTS,
474 label, {"name": name},
475 )
476 except Exception:
477 pass
478
479 def _load_json(self, path: Path) -> Any:
480 try:
481 return json.loads(Path(path).read_text(encoding="utf-8"))
482 except FileNotFoundError:
483 logger.warning("File not found: %s", path)
484 return {}
485 except json.JSONDecodeError as e:
486 logger.warning("Invalid JSON at %s: %s", path, e)
487 return {}
488
489 def _reset_stats(self) -> dict[str, int]:
490 self._stats = {"nodes": 0, "edges": 0}
491 return self._stats
492
493 def _merge_stats(self, other: dict[str, int]) -> None:
494 for k, v in other.items():
495 self._stats[k] = self._stats.get(k, 0) + v
496
--- navegador/ingestion/planopticon.py
+++ navegador/ingestion/planopticon.py
@@ -349,11 +349,11 @@
349 """
350 try:
351 self.store.query(cypher, {"src": src, "tgt": tgt})
352 self._stats["edges"] = self._stats.get("edges", 0) + 1
353 except Exception:
354 logger.warning("Could not create edge %s -[%s]-> %s", src, edge_type, tgt)
355
356 def _ingest_key_points(self, key_points: list[dict], source: str) -> None:
357 for kp in key_points:
358 point = (kp.get("point") or "").strip()
359 if not point:
@@ -472,24 +472,17 @@
472 NodeLabel.WikiPage, {"name": source_id},
473 EdgeType.DOCUMENTS,
474 label, {"name": name},
475 )
476 except Exception:
477 logger.debug("Could not link %s to wiki page %s", name, source_id)
478
479 def _load_json(self, path: Path) -> Any:
480 return json.loads(Path(path).read_text(encoding="utf-8"))
 
 
 
 
 
 
 
481
482 def _reset_stats(self) -> dict[str, int]:
483 self._stats = {"nodes": 0, "edges": 0}
484 return self._stats
485
486 def _merge_stats(self, other: dict[str, int]) -> None:
487 for k, v in other.items():
488 self._stats[k] = self._stats.get(k, 0) + v
489
--- navegador/ingestion/wiki.py
+++ navegador/ingestion/wiki.py
@@ -145,11 +145,11 @@
145145
html_url = data.get("html_url", "")
146146
links = self._ingest_page(page_name, content, source="github", url=html_url)
147147
stats["pages"] += 1
148148
stats["links"] += links
149149
except Exception as exc:
150
- logger.debug("Skipping %s: %s", path, exc)
150
+ logger.warning("Skipping %s: %s", path, exc)
151151
152152
logger.info("Wiki (GitHub API): %d pages, %d links", stats["pages"], stats["links"])
153153
return stats
154154
155155
# ── Internal ──────────────────────────────────────────────────────────────
@@ -200,7 +200,7 @@
200200
NodeLabel.WikiPage, {"name": wiki_page_name},
201201
EdgeType.DOCUMENTS,
202202
label, {"name": node_name},
203203
)
204204
return 1
205
- except (ValueError, Exception):
205
+ except ValueError:
206206
return 0
207207
--- navegador/ingestion/wiki.py
+++ navegador/ingestion/wiki.py
@@ -145,11 +145,11 @@
145 html_url = data.get("html_url", "")
146 links = self._ingest_page(page_name, content, source="github", url=html_url)
147 stats["pages"] += 1
148 stats["links"] += links
149 except Exception as exc:
150 logger.debug("Skipping %s: %s", path, exc)
151
152 logger.info("Wiki (GitHub API): %d pages, %d links", stats["pages"], stats["links"])
153 return stats
154
155 # ── Internal ──────────────────────────────────────────────────────────────
@@ -200,7 +200,7 @@
200 NodeLabel.WikiPage, {"name": wiki_page_name},
201 EdgeType.DOCUMENTS,
202 label, {"name": node_name},
203 )
204 return 1
205 except (ValueError, Exception):
206 return 0
207
--- navegador/ingestion/wiki.py
+++ navegador/ingestion/wiki.py
@@ -145,11 +145,11 @@
145 html_url = data.get("html_url", "")
146 links = self._ingest_page(page_name, content, source="github", url=html_url)
147 stats["pages"] += 1
148 stats["links"] += links
149 except Exception as exc:
150 logger.warning("Skipping %s: %s", path, exc)
151
152 logger.info("Wiki (GitHub API): %d pages, %d links", stats["pages"], stats["links"])
153 return stats
154
155 # ── Internal ──────────────────────────────────────────────────────────────
@@ -200,7 +200,7 @@
200 NodeLabel.WikiPage, {"name": wiki_page_name},
201 EdgeType.DOCUMENTS,
202 label, {"name": node_name},
203 )
204 return 1
205 except ValueError:
206 return 0
207
--- pyproject.toml
+++ pyproject.toml
@@ -53,10 +53,11 @@
5353
"redis>=5.0.0",
5454
]
5555
dev = [
5656
"pytest>=7.3.0",
5757
"pytest-cov>=4.1.0",
58
+ "pytest-asyncio>=0.23.0",
5859
"ruff>=0.1.0",
5960
"mypy>=1.3.0",
6061
]
6162
docs = [
6263
"mkdocs-material>=9.0.0",
6364
--- pyproject.toml
+++ pyproject.toml
@@ -53,10 +53,11 @@
53 "redis>=5.0.0",
54 ]
55 dev = [
56 "pytest>=7.3.0",
57 "pytest-cov>=4.1.0",
 
58 "ruff>=0.1.0",
59 "mypy>=1.3.0",
60 ]
61 docs = [
62 "mkdocs-material>=9.0.0",
63
--- pyproject.toml
+++ pyproject.toml
@@ -53,10 +53,11 @@
53 "redis>=5.0.0",
54 ]
55 dev = [
56 "pytest>=7.3.0",
57 "pytest-cov>=4.1.0",
58 "pytest-asyncio>=0.23.0",
59 "ruff>=0.1.0",
60 "mypy>=1.3.0",
61 ]
62 docs = [
63 "mkdocs-material>=9.0.0",
64
--- tests/test_ingestion_planopticon.py
+++ tests/test_ingestion_planopticon.py
@@ -2,10 +2,12 @@
22
33
import json
44
import tempfile
55
from pathlib import Path
66
from unittest.mock import MagicMock
7
+
8
+import pytest
79
810
from navegador.graph.schema import NodeLabel
911
from navegador.ingestion.planopticon import (
1012
EDGE_MAP,
1113
NODE_TYPE_MAP,
@@ -214,15 +216,15 @@
214216
p = Path(tmpdir) / "kg.json"
215217
p.write_text(json.dumps(data))
216218
stats = ingester.ingest_kg(p)
217219
assert stats["edges"] == 0
218220
219
- def test_missing_file_returns_empty_stats(self):
221
+ def test_missing_file_raises(self):
220222
store = _make_store()
221223
ingester = PlanopticonIngester(store)
222
- stats = ingester.ingest_kg("/nonexistent/kg.json")
223
- assert stats["nodes"] == 0
224
+ with pytest.raises(FileNotFoundError):
225
+ ingester.ingest_kg("/nonexistent/kg.json")
224226
225227
def test_returns_stats_dict(self):
226228
store = _make_store()
227229
ingester = PlanopticonIngester(store)
228230
with tempfile.TemporaryDirectory() as tmpdir:
@@ -409,11 +411,11 @@
409411
p = Path(tmpdir) / "batch.json"
410412
p.write_text(json.dumps(batch))
411413
stats = ingester.ingest_batch(p)
412414
assert "nodes" in stats
413415
414
- def test_skips_missing_manifest_gracefully(self):
416
+ def test_missing_manifest_raises(self):
415417
store = _make_store()
416418
ingester = PlanopticonIngester(store)
417419
with tempfile.TemporaryDirectory() as tmpdir:
418420
batch = {
419421
"videos": [
@@ -420,12 +422,12 @@
420422
{"status": "completed", "manifest_path": "nonexistent.json"},
421423
]
422424
}
423425
p = Path(tmpdir) / "batch.json"
424426
p.write_text(json.dumps(batch))
425
- # Should not raise
426
- ingester.ingest_batch(p)
427
+ with pytest.raises(FileNotFoundError):
428
+ ingester.ingest_batch(p)
427429
428430
def test_merges_stats_across_videos(self):
429431
store = _make_store()
430432
ingester = PlanopticonIngester(store)
431433
with tempfile.TemporaryDirectory() as tmpdir:
@@ -461,24 +463,24 @@
461463
ingester._merge_stats({"nodes": 3, "edges": 2, "pages": 1})
462464
assert ingester._stats["nodes"] == 5
463465
assert ingester._stats["edges"] == 3
464466
assert ingester._stats["pages"] == 1
465467
466
- def test_load_json_missing_file(self):
468
+ def test_load_json_missing_file_raises(self):
467469
store = _make_store()
468470
ingester = PlanopticonIngester(store)
469
- result = ingester._load_json(Path("/nonexistent/file.json"))
470
- assert result == {}
471
+ with pytest.raises(FileNotFoundError):
472
+ ingester._load_json(Path("/nonexistent/file.json"))
471473
472
- def test_load_json_invalid_json(self):
474
+ def test_load_json_invalid_json_raises(self):
473475
store = _make_store()
474476
ingester = PlanopticonIngester(store)
475477
with tempfile.TemporaryDirectory() as tmpdir:
476478
p = Path(tmpdir) / "bad.json"
477479
p.write_text("{ not valid json }")
478
- result = ingester._load_json(p)
479
- assert result == {}
480
+ with pytest.raises((json.JSONDecodeError, ValueError)):
481
+ ingester._load_json(p)
480482
481483
482484
# ── ingest_interchange relationship/source branches (lines 201, 209) ──────────
483485
484486
class TestInterchangeRelationshipsAndSources:
485487
--- tests/test_ingestion_planopticon.py
+++ tests/test_ingestion_planopticon.py
@@ -2,10 +2,12 @@
2
3 import json
4 import tempfile
5 from pathlib import Path
6 from unittest.mock import MagicMock
 
 
7
8 from navegador.graph.schema import NodeLabel
9 from navegador.ingestion.planopticon import (
10 EDGE_MAP,
11 NODE_TYPE_MAP,
@@ -214,15 +216,15 @@
214 p = Path(tmpdir) / "kg.json"
215 p.write_text(json.dumps(data))
216 stats = ingester.ingest_kg(p)
217 assert stats["edges"] == 0
218
219 def test_missing_file_returns_empty_stats(self):
220 store = _make_store()
221 ingester = PlanopticonIngester(store)
222 stats = ingester.ingest_kg("/nonexistent/kg.json")
223 assert stats["nodes"] == 0
224
225 def test_returns_stats_dict(self):
226 store = _make_store()
227 ingester = PlanopticonIngester(store)
228 with tempfile.TemporaryDirectory() as tmpdir:
@@ -409,11 +411,11 @@
409 p = Path(tmpdir) / "batch.json"
410 p.write_text(json.dumps(batch))
411 stats = ingester.ingest_batch(p)
412 assert "nodes" in stats
413
414 def test_skips_missing_manifest_gracefully(self):
415 store = _make_store()
416 ingester = PlanopticonIngester(store)
417 with tempfile.TemporaryDirectory() as tmpdir:
418 batch = {
419 "videos": [
@@ -420,12 +422,12 @@
420 {"status": "completed", "manifest_path": "nonexistent.json"},
421 ]
422 }
423 p = Path(tmpdir) / "batch.json"
424 p.write_text(json.dumps(batch))
425 # Should not raise
426 ingester.ingest_batch(p)
427
428 def test_merges_stats_across_videos(self):
429 store = _make_store()
430 ingester = PlanopticonIngester(store)
431 with tempfile.TemporaryDirectory() as tmpdir:
@@ -461,24 +463,24 @@
461 ingester._merge_stats({"nodes": 3, "edges": 2, "pages": 1})
462 assert ingester._stats["nodes"] == 5
463 assert ingester._stats["edges"] == 3
464 assert ingester._stats["pages"] == 1
465
466 def test_load_json_missing_file(self):
467 store = _make_store()
468 ingester = PlanopticonIngester(store)
469 result = ingester._load_json(Path("/nonexistent/file.json"))
470 assert result == {}
471
472 def test_load_json_invalid_json(self):
473 store = _make_store()
474 ingester = PlanopticonIngester(store)
475 with tempfile.TemporaryDirectory() as tmpdir:
476 p = Path(tmpdir) / "bad.json"
477 p.write_text("{ not valid json }")
478 result = ingester._load_json(p)
479 assert result == {}
480
481
482 # ── ingest_interchange relationship/source branches (lines 201, 209) ──────────
483
484 class TestInterchangeRelationshipsAndSources:
485
--- tests/test_ingestion_planopticon.py
+++ tests/test_ingestion_planopticon.py
@@ -2,10 +2,12 @@
2
3 import json
4 import tempfile
5 from pathlib import Path
6 from unittest.mock import MagicMock
7
8 import pytest
9
10 from navegador.graph.schema import NodeLabel
11 from navegador.ingestion.planopticon import (
12 EDGE_MAP,
13 NODE_TYPE_MAP,
@@ -214,15 +216,15 @@
216 p = Path(tmpdir) / "kg.json"
217 p.write_text(json.dumps(data))
218 stats = ingester.ingest_kg(p)
219 assert stats["edges"] == 0
220
221 def test_missing_file_raises(self):
222 store = _make_store()
223 ingester = PlanopticonIngester(store)
224 with pytest.raises(FileNotFoundError):
225 ingester.ingest_kg("/nonexistent/kg.json")
226
227 def test_returns_stats_dict(self):
228 store = _make_store()
229 ingester = PlanopticonIngester(store)
230 with tempfile.TemporaryDirectory() as tmpdir:
@@ -409,11 +411,11 @@
411 p = Path(tmpdir) / "batch.json"
412 p.write_text(json.dumps(batch))
413 stats = ingester.ingest_batch(p)
414 assert "nodes" in stats
415
416 def test_missing_manifest_raises(self):
417 store = _make_store()
418 ingester = PlanopticonIngester(store)
419 with tempfile.TemporaryDirectory() as tmpdir:
420 batch = {
421 "videos": [
@@ -420,12 +422,12 @@
422 {"status": "completed", "manifest_path": "nonexistent.json"},
423 ]
424 }
425 p = Path(tmpdir) / "batch.json"
426 p.write_text(json.dumps(batch))
427 with pytest.raises(FileNotFoundError):
428 ingester.ingest_batch(p)
429
430 def test_merges_stats_across_videos(self):
431 store = _make_store()
432 ingester = PlanopticonIngester(store)
433 with tempfile.TemporaryDirectory() as tmpdir:
@@ -461,24 +463,24 @@
463 ingester._merge_stats({"nodes": 3, "edges": 2, "pages": 1})
464 assert ingester._stats["nodes"] == 5
465 assert ingester._stats["edges"] == 3
466 assert ingester._stats["pages"] == 1
467
468 def test_load_json_missing_file_raises(self):
469 store = _make_store()
470 ingester = PlanopticonIngester(store)
471 with pytest.raises(FileNotFoundError):
472 ingester._load_json(Path("/nonexistent/file.json"))
473
474 def test_load_json_invalid_json_raises(self):
475 store = _make_store()
476 ingester = PlanopticonIngester(store)
477 with tempfile.TemporaryDirectory() as tmpdir:
478 p = Path(tmpdir) / "bad.json"
479 p.write_text("{ not valid json }")
480 with pytest.raises((json.JSONDecodeError, ValueError)):
481 ingester._load_json(p)
482
483
484 # ── ingest_interchange relationship/source branches (lines 201, 209) ──────────
485
486 class TestInterchangeRelationshipsAndSources:
487
--- tests/test_ingestion_wiki.py
+++ tests/test_ingestion_wiki.py
@@ -165,17 +165,24 @@
165165
ingester = WikiIngester(store)
166166
result = ingester._try_link("wiki page", "MyService")
167167
assert result == 1
168168
store.create_edge.assert_called_once()
169169
170
- def test_returns_zero_on_exception(self):
170
+ def test_returns_zero_on_unknown_label(self):
171
+ store = MagicMock()
172
+ store.query.return_value = MagicMock(result_set=[["UnknownLabel", "node"]])
173
+ ingester = WikiIngester(store)
174
+ result = ingester._try_link("page", "node")
175
+ assert result == 0
176
+
177
+ def test_propagates_store_error(self):
171178
store = MagicMock()
172179
store.query.return_value = MagicMock(result_set=[["Concept", "node"]])
173180
store.create_edge.side_effect = Exception("DB error")
174181
ingester = WikiIngester(store)
175
- result = ingester._try_link("page", "node")
176
- assert result == 0
182
+ with pytest.raises(Exception, match="DB error"):
183
+ ingester._try_link("page", "node")
177184
178185
179186
# ── GitHub clone (ingest_github) ──────────────────────────────────────────────
180187
181188
class TestIngestGithub:
182189
--- tests/test_ingestion_wiki.py
+++ tests/test_ingestion_wiki.py
@@ -165,17 +165,24 @@
165 ingester = WikiIngester(store)
166 result = ingester._try_link("wiki page", "MyService")
167 assert result == 1
168 store.create_edge.assert_called_once()
169
170 def test_returns_zero_on_exception(self):
 
 
 
 
 
 
 
171 store = MagicMock()
172 store.query.return_value = MagicMock(result_set=[["Concept", "node"]])
173 store.create_edge.side_effect = Exception("DB error")
174 ingester = WikiIngester(store)
175 result = ingester._try_link("page", "node")
176 assert result == 0
177
178
179 # ── GitHub clone (ingest_github) ──────────────────────────────────────────────
180
181 class TestIngestGithub:
182
--- tests/test_ingestion_wiki.py
+++ tests/test_ingestion_wiki.py
@@ -165,17 +165,24 @@
165 ingester = WikiIngester(store)
166 result = ingester._try_link("wiki page", "MyService")
167 assert result == 1
168 store.create_edge.assert_called_once()
169
170 def test_returns_zero_on_unknown_label(self):
171 store = MagicMock()
172 store.query.return_value = MagicMock(result_set=[["UnknownLabel", "node"]])
173 ingester = WikiIngester(store)
174 result = ingester._try_link("page", "node")
175 assert result == 0
176
177 def test_propagates_store_error(self):
178 store = MagicMock()
179 store.query.return_value = MagicMock(result_set=[["Concept", "node"]])
180 store.create_edge.side_effect = Exception("DB error")
181 ingester = WikiIngester(store)
182 with pytest.raises(Exception, match="DB error"):
183 ingester._try_link("page", "node")
184
185
186 # ── GitHub clone (ingest_github) ──────────────────────────────────────────────
187
188 class TestIngestGithub:
189

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button