Coverage for src/qdrant_loader_core/graph/extractor/confluence.py: 100%
36 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:12 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:12 +0000
1from __future__ import annotations
3from typing import TYPE_CHECKING
5if TYPE_CHECKING:
6 from qdrant_loader.core.document import Document
8from qdrant_loader_core.graph.extractor.base_extractor import (
9 BaseEntityExtractor,
10)
11from qdrant_loader_core.graph.models import (
12 CoreEdgeType,
13 CoreNodeLabel,
14 GraphEdge,
15 GraphNode,
16 PersonInfo,
17)
20class ConfluenceEntityExtractor(BaseEntityExtractor):
21 """
22 Confluence graph extractor.
24 Extracts:
25 - Document node from page
26 - Person node from author
27 - Container node from space
28 - Parent / child page hierarchy
29 """
31 source_type = "confluence"
33 # ------------------------------------------------------------------
34 # Project / Space
35 # ------------------------------------------------------------------
37 def _project(
38 self,
39 doc: Document,
40 ) -> str | None:
41 return doc.metadata.get("space_key")
43 def _build_document_node(
44 self,
45 doc: Document,
46 project: str | None,
47 ) -> GraphNode:
48 """Build a document node for a Confluence page."""
50 return GraphNode(
51 id=doc.id,
52 label=CoreNodeLabel.DOCUMENT.value,
53 project=project,
54 properties={
55 "title": doc.title,
56 "url": doc.url,
57 "created_at": doc.metadata.get("created_at"),
58 "updated_at": doc.metadata.get("updated_at"),
59 "source_type": self.source_type,
60 },
61 )
63 # ------------------------------------------------------------------
64 # People
65 # ------------------------------------------------------------------
67 def _extract_people(
68 self,
69 doc: Document,
70 ) -> list[tuple[PersonInfo, str]]:
71 author = doc.metadata.get("author")
73 if not author:
74 return []
76 return [
77 (
78 PersonInfo(
79 id=str(author),
80 display_name=str(author),
81 ),
82 "author",
83 )
84 ]
86 # ------------------------------------------------------------------
87 # Space container
88 # ------------------------------------------------------------------
90 def _extract_container(
91 self,
92 doc: Document,
93 ) -> GraphNode | None:
94 space_key = doc.metadata.get("space_key")
96 if not space_key:
97 return None
99 return GraphNode(
100 id=f"space:{space_key}",
101 label=CoreNodeLabel.CONTAINER.value,
102 project=space_key,
103 properties={
104 "kind": "confluence_space",
105 "space_key": space_key,
106 },
107 )
109 # ------------------------------------------------------------------
110 # Labels
111 # ------------------------------------------------------------------
113 def _extract_labels(
114 self,
115 doc: Document,
116 ) -> list[GraphNode]:
117 project = self._project(doc)
119 labels = doc.metadata.get("labels", [])
121 return [
122 GraphNode(
123 id=f"label:{label}",
124 label=CoreNodeLabel.LABEL.value,
125 project=project,
126 properties={
127 "name": label,
128 },
129 )
130 for label in labels
131 ]
133 # ------------------------------------------------------------------
134 # Hierarchy
135 # ------------------------------------------------------------------
137 def _extract_source_specific(
138 self,
139 doc: Document,
140 ) -> tuple[list[GraphNode], list[GraphEdge]]:
141 project = self._project(doc)
143 edges: list[GraphEdge] = []
145 parent_id = doc.metadata.get("parent_id")
147 if parent_id:
148 edges.append(
149 GraphEdge(
150 source=doc.id,
151 target=str(parent_id),
152 edge_type=CoreEdgeType.PART_OF.value,
153 project=project,
154 properties={
155 "kind": "page_child",
156 },
157 )
158 )
160 for child in doc.metadata.get("children", []):
161 child_id = child.get("id")
163 if not child_id:
164 continue
166 edges.append(
167 GraphEdge(
168 source=doc.id,
169 target=str(child_id),
170 edge_type=CoreEdgeType.HAS_CHILD.value,
171 project=project,
172 properties={
173 "kind": "page_child",
174 },
175 )
176 )
178 return [], edges