chore(lint): fix ruff violations on Neo4j files
Pre-existing ruff violations surfaced by CI on PR #188: - TC001: move runtime `Neo4jDriver` imports into `TYPE_CHECKING` blocks (queries.py, chunk_writer.py, schema.py, tree_reader.py, tree_writer.py) - SIM117: combine nested `async with` in chunk_writer.write_chunks and tree_writer.write_document - SIM105: replace try/except/pass with `contextlib.suppress` in tree_writer._element_props - F401: remove unused `Neo4jDriver` import in main._init_neo4j - RUF100: remove unused `# noqa: E402` in tests/neo4j/conftest.py - I001: sort imports in tests/neo4j/test_chunk_writer.py and test_tree_writer.py Zero behaviour change. `ruff check .` now passes cleanly.
This commit is contained in:
parent
03e5dfac17
commit
bd73c4bbfd
9 changed files with 209 additions and 193 deletions
|
|
@ -16,8 +16,9 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
from infra.neo4j.driver import Neo4jDriver
|
from infra.neo4j.driver import Neo4jDriver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -63,8 +64,10 @@ async def write_chunks(
|
||||||
if ref:
|
if ref:
|
||||||
derived_rows.append({"chunk_id": cid, "doc_id": doc_id, "self_ref": ref})
|
derived_rows.append({"chunk_id": cid, "doc_id": doc_id, "self_ref": ref})
|
||||||
|
|
||||||
async with neo.driver.session(database=neo.database) as session:
|
async with (
|
||||||
async with await session.begin_transaction() as tx:
|
neo.driver.session(database=neo.database) as session,
|
||||||
|
await session.begin_transaction() as tx,
|
||||||
|
):
|
||||||
# Replace existing chunks.
|
# Replace existing chunks.
|
||||||
await tx.run(
|
await tx.run(
|
||||||
"""
|
"""
|
||||||
|
|
@ -73,9 +76,7 @@ async def write_chunks(
|
||||||
""",
|
""",
|
||||||
doc_id=doc_id,
|
doc_id=doc_id,
|
||||||
)
|
)
|
||||||
await tx.run(
|
await tx.run("MATCH (c:Chunk {doc_id: $doc_id}) DETACH DELETE c", doc_id=doc_id)
|
||||||
"MATCH (c:Chunk {doc_id: $doc_id}) DETACH DELETE c", doc_id=doc_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if chunk_rows:
|
if chunk_rows:
|
||||||
await tx.run(
|
await tx.run(
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
from infra.neo4j.driver import Neo4jDriver
|
from infra.neo4j.driver import Neo4jDriver
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,21 @@ this multiple times is safe — it's the contract integration tests rely on.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
from infra.neo4j.driver import Neo4jDriver
|
from infra.neo4j.driver import Neo4jDriver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
CONSTRAINTS: tuple[str, ...] = (
|
CONSTRAINTS: tuple[str, ...] = (
|
||||||
"CREATE CONSTRAINT document_id IF NOT EXISTS "
|
"CREATE CONSTRAINT document_id IF NOT EXISTS FOR (d:Document) REQUIRE d.id IS UNIQUE",
|
||||||
"FOR (d:Document) REQUIRE d.id IS UNIQUE",
|
|
||||||
"CREATE CONSTRAINT element_composite IF NOT EXISTS "
|
"CREATE CONSTRAINT element_composite IF NOT EXISTS "
|
||||||
"FOR (e:Element) REQUIRE (e.doc_id, e.self_ref) IS UNIQUE",
|
"FOR (e:Element) REQUIRE (e.doc_id, e.self_ref) IS UNIQUE",
|
||||||
"CREATE CONSTRAINT page_composite IF NOT EXISTS "
|
"CREATE CONSTRAINT page_composite IF NOT EXISTS "
|
||||||
"FOR (p:Page) REQUIRE (p.doc_id, p.page_no) IS UNIQUE",
|
"FOR (p:Page) REQUIRE (p.doc_id, p.page_no) IS UNIQUE",
|
||||||
"CREATE CONSTRAINT chunk_id IF NOT EXISTS "
|
"CREATE CONSTRAINT chunk_id IF NOT EXISTS FOR (c:Chunk) REQUIRE c.id IS UNIQUE",
|
||||||
"FOR (c:Chunk) REQUIRE c.id IS UNIQUE",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
INDEXES: tuple[str, ...] = (
|
INDEXES: tuple[str, ...] = (
|
||||||
|
|
@ -30,8 +30,7 @@ INDEXES: tuple[str, ...] = (
|
||||||
)
|
)
|
||||||
|
|
||||||
FULLTEXT_INDEXES: tuple[str, ...] = (
|
FULLTEXT_INDEXES: tuple[str, ...] = (
|
||||||
"CREATE FULLTEXT INDEX element_text IF NOT EXISTS "
|
"CREATE FULLTEXT INDEX element_text IF NOT EXISTS FOR (e:Element) ON EACH [e.text]",
|
||||||
"FOR (e:Element) ON EACH [e.text]",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ nodes directly.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
from infra.neo4j.driver import Neo4jDriver
|
from infra.neo4j.driver import Neo4jDriver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -57,10 +59,6 @@ async def delete_document(neo: Neo4jDriver, doc_id: str) -> int:
|
||||||
)
|
)
|
||||||
record = await result.single()
|
record = await result.single()
|
||||||
# Also clean up orphan elements and pages tagged with this doc_id.
|
# Also clean up orphan elements and pages tagged with this doc_id.
|
||||||
await session.run(
|
await session.run("MATCH (e:Element {doc_id: $doc_id}) DETACH DELETE e", doc_id=doc_id)
|
||||||
"MATCH (e:Element {doc_id: $doc_id}) DETACH DELETE e", doc_id=doc_id
|
await session.run("MATCH (p:Page {doc_id: $doc_id}) DETACH DELETE p", doc_id=doc_id)
|
||||||
)
|
|
||||||
await session.run(
|
|
||||||
"MATCH (p:Page {doc_id: $doc_id}) DETACH DELETE p", doc_id=doc_id
|
|
||||||
)
|
|
||||||
return int(record["removed"]) if record else 0
|
return int(record["removed"]) if record else 0
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ graph nodes is deferred to v0.6 (see docs/design/neo4j-integration.md §2).
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
from infra.neo4j.driver import Neo4jDriver
|
from infra.neo4j.driver import Neo4jDriver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -96,10 +98,8 @@ def _element_props(item: dict[str, Any], doc_id: str) -> dict[str, Any]:
|
||||||
props["caption"] = item.get("caption")
|
props["caption"] = item.get("caption")
|
||||||
if item.get("data") and isinstance(item["data"], dict):
|
if item.get("data") and isinstance(item["data"], dict):
|
||||||
# Tables carry cell layout under data; stringify to keep the schema flat.
|
# Tables carry cell layout under data; stringify to keep the schema flat.
|
||||||
try:
|
with contextlib.suppress(TypeError, ValueError):
|
||||||
props["cells_json"] = json.dumps(item["data"])
|
props["cells_json"] = json.dumps(item["data"])
|
||||||
except (TypeError, ValueError):
|
|
||||||
pass
|
|
||||||
return props
|
return props
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ async def write_document(
|
||||||
Fails fast (exception propagates) if Neo4j is unavailable — per design §8.5.
|
Fails fast (exception propagates) if Neo4j is unavailable — per design §8.5.
|
||||||
"""
|
"""
|
||||||
doc_data = json.loads(document_json)
|
doc_data = json.loads(document_json)
|
||||||
ingested_at = datetime.now(tz=timezone.utc).isoformat()
|
ingested_at = datetime.now(tz=UTC).isoformat()
|
||||||
|
|
||||||
elements: list[dict[str, Any]] = []
|
elements: list[dict[str, Any]] = []
|
||||||
for _, item in _iter_items(doc_data):
|
for _, item in _iter_items(doc_data):
|
||||||
|
|
@ -179,8 +179,10 @@ async def write_document(
|
||||||
|
|
||||||
reading_order = _dfs_order(doc_data)
|
reading_order = _dfs_order(doc_data)
|
||||||
|
|
||||||
async with neo.driver.session(database=neo.database) as session:
|
async with (
|
||||||
async with await session.begin_transaction() as tx:
|
neo.driver.session(database=neo.database) as session,
|
||||||
|
await session.begin_transaction() as tx,
|
||||||
|
):
|
||||||
# 1. Wipe existing graph for this doc_id (replace strategy).
|
# 1. Wipe existing graph for this doc_id (replace strategy).
|
||||||
await tx.run(
|
await tx.run(
|
||||||
"MATCH (d:Document {id: $doc_id}) "
|
"MATCH (d:Document {id: $doc_id}) "
|
||||||
|
|
@ -189,12 +191,8 @@ async def write_document(
|
||||||
doc_id=doc_id,
|
doc_id=doc_id,
|
||||||
)
|
)
|
||||||
# Also wipe orphan elements/chunks that may still reference this doc.
|
# Also wipe orphan elements/chunks that may still reference this doc.
|
||||||
await tx.run(
|
await tx.run("MATCH (e:Element {doc_id: $doc_id}) DETACH DELETE e", doc_id=doc_id)
|
||||||
"MATCH (e:Element {doc_id: $doc_id}) DETACH DELETE e", doc_id=doc_id
|
await tx.run("MATCH (p:Page {doc_id: $doc_id}) DETACH DELETE p", doc_id=doc_id)
|
||||||
)
|
|
||||||
await tx.run(
|
|
||||||
"MATCH (p:Page {doc_id: $doc_id}) DETACH DELETE p", doc_id=doc_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Document node (carries the verbatim JSON for TreeReader).
|
# 2. Document node (carries the verbatim JSON for TreeReader).
|
||||||
await tx.run(
|
await tx.run(
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ async def _init_neo4j():
|
||||||
logger.info("Neo4j disabled (NEO4J_URI not set)")
|
logger.info("Neo4j disabled (NEO4J_URI not set)")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
from infra.neo4j import Neo4jDriver, bootstrap_schema, get_driver
|
from infra.neo4j import bootstrap_schema, get_driver
|
||||||
|
|
||||||
try:
|
try:
|
||||||
neo = await get_driver(
|
neo = await get_driver(
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import pytest
|
||||||
# (e.g. local dev without the dependency installed).
|
# (e.g. local dev without the dependency installed).
|
||||||
pytest.importorskip("neo4j")
|
pytest.importorskip("neo4j")
|
||||||
|
|
||||||
from infra.neo4j import close_driver, get_driver # noqa: E402
|
from infra.neo4j import close_driver, get_driver
|
||||||
|
|
||||||
|
|
||||||
def _cfg() -> tuple[str, str, str]:
|
def _cfg() -> tuple[str, str, str]:
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from infra.neo4j import fetch_graph, write_chunks, write_document
|
||||||
from infra.neo4j.schema import bootstrap_schema
|
from infra.neo4j.schema import bootstrap_schema
|
||||||
from tests.neo4j.test_tree_writer import FIXTURE
|
from tests.neo4j.test_tree_writer import FIXTURE
|
||||||
|
|
||||||
|
|
||||||
CHUNKS = [
|
CHUNKS = [
|
||||||
{
|
{
|
||||||
"text": "Introduction. First paragraph on page 1.",
|
"text": "Introduction. First paragraph on page 1.",
|
||||||
|
|
@ -66,8 +65,7 @@ async def test_write_chunks_and_derived_from(neo4j_driver):
|
||||||
for idx, expected in [(0, 2), (1, 1)]:
|
for idx, expected in [(0, 2), (1, 1)]:
|
||||||
cnt = await (
|
cnt = await (
|
||||||
await s.run(
|
await s.run(
|
||||||
"MATCH (c:Chunk {id: $cid})-[:DERIVED_FROM]->(e:Element) "
|
"MATCH (c:Chunk {id: $cid})-[:DERIVED_FROM]->(e:Element) RETURN count(e) AS n",
|
||||||
"RETURN count(e) AS n",
|
|
||||||
cid=f"doc-fixture::chunk::{idx}",
|
cid=f"doc-fixture::chunk::{idx}",
|
||||||
)
|
)
|
||||||
).single()
|
).single()
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import json
|
||||||
from infra.neo4j import read_document_json, write_document
|
from infra.neo4j import read_document_json, write_document
|
||||||
from infra.neo4j.schema import bootstrap_schema
|
from infra.neo4j.schema import bootstrap_schema
|
||||||
|
|
||||||
|
|
||||||
FIXTURE = {
|
FIXTURE = {
|
||||||
"name": "fixture.pdf",
|
"name": "fixture.pdf",
|
||||||
"pages": {
|
"pages": {
|
||||||
|
|
@ -89,41 +88,59 @@ async def test_write_creates_expected_structure(neo4j_driver):
|
||||||
assert result.pages_written == 2
|
assert result.pages_written == 2
|
||||||
|
|
||||||
async with neo4j_driver.driver.session(database=neo4j_driver.database) as s:
|
async with neo4j_driver.driver.session(database=neo4j_driver.database) as s:
|
||||||
assert await _count(
|
assert (
|
||||||
|
await _count(
|
||||||
s,
|
s,
|
||||||
"MATCH (d:Document {id: $id}) RETURN count(d) AS n",
|
"MATCH (d:Document {id: $id}) RETURN count(d) AS n",
|
||||||
id="doc-fixture",
|
id="doc-fixture",
|
||||||
) == 1
|
)
|
||||||
assert await _count(
|
== 1
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
await _count(
|
||||||
s,
|
s,
|
||||||
"MATCH (:Document {id: $id})-[:HAS_ROOT]->(e:Element) RETURN count(e) AS n",
|
"MATCH (:Document {id: $id})-[:HAS_ROOT]->(e:Element) RETURN count(e) AS n",
|
||||||
id="doc-fixture",
|
id="doc-fixture",
|
||||||
) == 4
|
)
|
||||||
assert await _count(
|
== 4
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
await _count(
|
||||||
s,
|
s,
|
||||||
"MATCH (e:Element:SectionHeader {doc_id: $id, self_ref: '#/texts/0'}) "
|
"MATCH (e:Element:SectionHeader {doc_id: $id, self_ref: '#/texts/0'}) "
|
||||||
"RETURN count(e) AS n",
|
"RETURN count(e) AS n",
|
||||||
id="doc-fixture",
|
id="doc-fixture",
|
||||||
) == 1
|
)
|
||||||
assert await _count(
|
== 1
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
await _count(
|
||||||
s,
|
s,
|
||||||
"MATCH (e:Element:Table {doc_id: $id}) RETURN count(e) AS n",
|
"MATCH (e:Element:Table {doc_id: $id}) RETURN count(e) AS n",
|
||||||
id="doc-fixture",
|
id="doc-fixture",
|
||||||
) == 1
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
# Reading-order chain: 3 NEXT edges for 4 elements.
|
# Reading-order chain: 3 NEXT edges for 4 elements.
|
||||||
assert await _count(
|
assert (
|
||||||
|
await _count(
|
||||||
s,
|
s,
|
||||||
"MATCH (a:Element {doc_id: $id})-[:NEXT]->(b:Element {doc_id: $id}) "
|
"MATCH (a:Element {doc_id: $id})-[:NEXT]->(b:Element {doc_id: $id}) "
|
||||||
"RETURN count(*) AS n",
|
"RETURN count(*) AS n",
|
||||||
id="doc-fixture",
|
id="doc-fixture",
|
||||||
) == 3
|
)
|
||||||
|
== 3
|
||||||
|
)
|
||||||
# ON_PAGE: one per element with prov.
|
# ON_PAGE: one per element with prov.
|
||||||
assert await _count(
|
assert (
|
||||||
|
await _count(
|
||||||
s,
|
s,
|
||||||
"MATCH (:Element {doc_id: $id})-[:ON_PAGE]->(:Page {doc_id: $id}) "
|
"MATCH (:Element {doc_id: $id})-[:ON_PAGE]->(:Page {doc_id: $id}) "
|
||||||
"RETURN count(*) AS n",
|
"RETURN count(*) AS n",
|
||||||
id="doc-fixture",
|
id="doc-fixture",
|
||||||
) == 4
|
)
|
||||||
|
== 4
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_rewrite_is_idempotent_replace(neo4j_driver):
|
async def test_rewrite_is_idempotent_replace(neo4j_driver):
|
||||||
|
|
@ -145,14 +162,18 @@ async def test_rewrite_is_idempotent_replace(neo4j_driver):
|
||||||
)
|
)
|
||||||
|
|
||||||
async with neo4j_driver.driver.session(database=neo4j_driver.database) as s:
|
async with neo4j_driver.driver.session(database=neo4j_driver.database) as s:
|
||||||
assert await _count(
|
assert (
|
||||||
s, "MATCH (d:Document {id: $id}) RETURN count(d) AS n", id="doc-fixture"
|
await _count(s, "MATCH (d:Document {id: $id}) RETURN count(d) AS n", id="doc-fixture")
|
||||||
) == 1
|
== 1
|
||||||
assert await _count(
|
)
|
||||||
|
assert (
|
||||||
|
await _count(
|
||||||
s,
|
s,
|
||||||
"MATCH (e:Element {doc_id: $id}) RETURN count(e) AS n",
|
"MATCH (e:Element {doc_id: $id}) RETURN count(e) AS n",
|
||||||
id="doc-fixture",
|
id="doc-fixture",
|
||||||
) == 4
|
)
|
||||||
|
== 4
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_reader_returns_verbatim_json(neo4j_driver):
|
async def test_reader_returns_verbatim_json(neo4j_driver):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue