Coverage for src/qdrant_loader_core/graph/extractor/publicdocs.py: 100%
42 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
4from urllib.parse import urlparse
6if TYPE_CHECKING:
7 from qdrant_loader.core.document import Document
9from qdrant_loader_core.graph.extractor.base_extractor import (
10 BaseEntityExtractor,
11)
12from qdrant_loader_core.graph.models import (
13 CoreEdgeType,
14 CoreNodeLabel,
15 GraphEdge,
16 GraphNode,
17)
20class PublicDocsEntityExtractor(BaseEntityExtractor):
21 """
22 Public documentation graph extractor.
24 Extracts:
25 - Document node from page
26 - Container node from website/domain
27 - LINKS_TO edges between pages
28 - Attachment nodes
29 """
31 source_type = "publicdocs"
33 # ------------------------------------------------------------------
34 # Project
35 # ------------------------------------------------------------------
37 def _project(
38 self,
39 doc: Document,
40 ) -> str | None:
41 url = getattr(doc, "url", None) or doc.metadata.get("url")
43 if not url:
44 return None
46 parsed = urlparse(url)
48 return parsed.netloc
50 # ------------------------------------------------------------------
51 # Build Document Node
52 # ------------------------------------------------------------------
53 def _build_document_node(
54 self,
55 doc: Document,
56 project: str | None,
57 ) -> GraphNode:
58 return GraphNode(
59 id=doc.id,
60 label=CoreNodeLabel.DOCUMENT.value,
61 project=project,
62 properties={
63 "title": doc.title,
64 "source_type": self.source_type,
65 },
66 )
68 # ------------------------------------------------------------------
69 # People
70 # ------------------------------------------------------------------
72 def _extract_people(
73 self,
74 doc: Document,
75 ) -> list:
76 return []
78 # ------------------------------------------------------------------
79 # Container
80 # ------------------------------------------------------------------
82 def _extract_container(
83 self,
84 doc: Document,
85 ) -> GraphNode | None:
86 url = getattr(doc, "url", None) or doc.metadata.get("url")
88 if not url:
89 return None
91 parsed = urlparse(url)
93 domain = parsed.netloc
95 if not domain:
96 return None
98 return GraphNode(
99 id=f"site:{domain}",
100 label=CoreNodeLabel.CONTAINER.value,
101 project=domain,
102 properties={
103 "kind": "website",
104 "domain": domain,
105 },
106 )
108 def _extract_labels(
109 self,
110 doc: Document,
111 ) -> list[GraphNode]:
112 project = self._project(doc)
114 return [
115 GraphNode(
116 id=f"label:{tag}",
117 label=CoreNodeLabel.LABEL.value,
118 project=project,
119 properties={"name": tag},
120 )
121 for tag in doc.metadata.get("tags", [])
122 ]
124 # ------------------------------------------------------------------
125 # Links + Attachments
126 # ------------------------------------------------------------------
128 def _extract_source_specific(
129 self,
130 doc: Document,
131 ) -> tuple[list[GraphNode], list[GraphEdge]]:
132 project = self._project(doc)
134 nodes: list[GraphNode] = []
135 edges: list[GraphEdge] = []
137 # --------------------------------------------------------------
138 # Internal links
139 # --------------------------------------------------------------
141 for link in doc.metadata.get("links", []):
142 edges.append(
143 GraphEdge(
144 source=doc.id,
145 target=link,
146 edge_type=CoreEdgeType.LINKS_TO.value,
147 project=project,
148 )
149 )
151 # --------------------------------------------------------------
152 # Attachments
153 # --------------------------------------------------------------
155 for attachment in doc.metadata.get("attachments", []):
156 attachment_id = attachment.get("id")
158 if not attachment_id:
159 continue
161 nodes.append(
162 GraphNode(
163 id=f"attachment:{attachment_id}",
164 label=CoreNodeLabel.ATTACHMENT.value,
165 project=project,
166 properties={
167 "filename": attachment.get("filename"),
168 "mime_type": attachment.get("mime_type"),
169 "download_url": attachment.get("download_url"),
170 },
171 )
172 )
174 edges.append(
175 GraphEdge(
176 source=doc.id,
177 target=f"attachment:{attachment_id}",
178 edge_type=CoreEdgeType.HAS_ATTACHMENT.value,
179 project=project,
180 )
181 )
183 return nodes, edges