Coverage for src/qdrant_loader/core/pipeline/source_processor.py: 62%

77 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-20 10:15 +0000

1"""Source processor for handling different source types.""" 

2 

3import asyncio 

4import inspect 

5from collections.abc import AsyncIterator, Callable, Mapping 

6from datetime import datetime 

7 

8from qdrant_loader.config.source_config import SourceConfig 

9from qdrant_loader.connectors.base import BaseConnector, ConnectorConfigurationError 

10from qdrant_loader.core.document import Document 

11from qdrant_loader.core.file_conversion import FileConversionConfig 

12from qdrant_loader.utils.logging import LoggingConfig 

13from qdrant_loader.utils.sensitive import sanitize_exception_message 

14 

15logger = LoggingConfig.get_logger(__name__) 

16 

17 

18class SourceProcessor: 

19 """Handles processing of different source types.""" 

20 

21 def __init__( 

22 self, 

23 shutdown_event: asyncio.Event | None = None, 

24 file_conversion_config: FileConversionConfig | None = None, 

25 ): 

26 self.shutdown_event = shutdown_event or asyncio.Event() 

27 self.file_conversion_config = file_conversion_config 

28 

29 async def process_source_type( 

30 self, 

31 source_configs: Mapping[str, SourceConfig], 

32 connector_factory: Callable[[SourceConfig], BaseConnector], 

33 source_type: str, 

34 ) -> list[Document]: 

35 """Process documents from a specific source type. 

36 

37 Args: 

38 source_configs: Mapping of source name to source configuration 

39 connector_factory: Factory function that creates a connector from a source config 

40 source_type: The type of source being processed 

41 

42 Returns: 

43 List of documents from all sources of this type 

44 """ 

45 logger.debug(f"Processing {source_type} sources: {list(source_configs.keys())}") 

46 

47 all_documents = [] 

48 

49 for source_name, source_config in source_configs.items(): 

50 if self.shutdown_event.is_set(): 

51 logger.info( 

52 f"Shutdown requested, skipping {source_type} source: {source_name}" 

53 ) 

54 break 

55 

56 try: 

57 logger.debug(f"Processing {source_type} source: {source_name}") 

58 

59 # Create connector instance and use as async context manager 

60 maybe_connector = connector_factory(source_config) 

61 connector = ( 

62 await maybe_connector 

63 if asyncio.iscoroutine(maybe_connector) 

64 or inspect.isawaitable(maybe_connector) 

65 else maybe_connector 

66 ) 

67 

68 # Set file conversion config if available and connector supports it 

69 if ( 

70 self.file_conversion_config 

71 and hasattr(connector, "set_file_conversion_config") 

72 and hasattr(source_config, "enable_file_conversion") 

73 and source_config.enable_file_conversion 

74 ): 

75 logger.debug( 

76 f"Setting file conversion config for {source_type} source: {source_name}" 

77 ) 

78 connector.set_file_conversion_config(self.file_conversion_config) 

79 

80 # Use the connector as an async context manager to ensure proper initialization 

81 async with connector: 

82 documents = [] 

83 if isinstance(connector, BaseConnector): 

84 try: 

85 async for document in connector.stream_documents(): 

86 documents.append(document) 

87 except NotImplementedError: 

88 # Connector does not implement streaming; fall back to eager fetch 

89 documents = await connector.get_documents() 

90 else: 

91 documents = await connector.get_documents() 

92 

93 logger.debug( 

94 f"Retrieved {len(documents)} documents from {source_type} source: {source_name}" 

95 ) 

96 all_documents.extend(documents) 

97 

98 except ConnectorConfigurationError: 

99 # Fatal configuration error – re-raise immediately so the 

100 # pipeline stops with a clear message instead of silently 

101 # producing 0 documents. 

102 raise 

103 except Exception as e: 

104 safe_error = sanitize_exception_message(e) 

105 logger.error( 

106 f"Failed to process {source_type} source {source_name}: {safe_error}", 

107 error_type=type(e).__name__, 

108 ) 

109 # Continue processing other sources even if one fails 

110 continue 

111 

112 if all_documents: 

113 logger.info( 

114 f"📥 {source_type}: {len(all_documents)} documents from {len(source_configs)} sources" 

115 ) 

116 return all_documents 

117 

118 async def stream_source_documents( 

119 self, 

120 source_configs: Mapping[str, SourceConfig], 

121 connector_factory: Callable[[SourceConfig], BaseConnector], 

122 source_type: str, 

123 since: datetime | None = None, 

124 ) -> AsyncIterator[Document]: 

125 """Stream documents from a specific source type (WS-1). 

126 

127 Yields documents one at a time as they are fetched from the source. 

128 """ 

129 logger.debug(f"Streaming {source_type} sources: {list(source_configs.keys())}") 

130 

131 for source_name, source_config in source_configs.items(): 

132 if self.shutdown_event.is_set(): 

133 logger.info( 

134 f"Shutdown requested, skipping {source_type} source: {source_name}" 

135 ) 

136 break 

137 

138 try: 

139 logger.debug(f"Streaming {source_type} source: {source_name}") 

140 

141 maybe_connector = connector_factory(source_config) 

142 connector = ( 

143 await maybe_connector 

144 if asyncio.iscoroutine(maybe_connector) 

145 or inspect.isawaitable(maybe_connector) 

146 else maybe_connector 

147 ) 

148 

149 if ( 

150 self.file_conversion_config 

151 and hasattr(connector, "set_file_conversion_config") 

152 and hasattr(source_config, "enable_file_conversion") 

153 and source_config.enable_file_conversion 

154 ): 

155 logger.debug( 

156 f"Setting file conversion config for {source_type} source: {source_name}" 

157 ) 

158 connector.set_file_conversion_config(self.file_conversion_config) 

159 

160 async with connector: 

161 document_count = 0 

162 async for document in connector.stream_documents(since=since): 

163 yield document 

164 document_count += 1 

165 if document_count % 100 == 0: 

166 logger.debug( 

167 f"Streamed {document_count} documents from {source_type} source: {source_name}" 

168 ) 

169 

170 if document_count > 0: 

171 logger.info( 

172 f"✅ Streamed {document_count} documents from {source_type} source: {source_name}" 

173 ) 

174 

175 except ConnectorConfigurationError: 

176 raise 

177 except Exception as e: 

178 safe_error = sanitize_exception_message(e) 

179 logger.error( 

180 f"Failed to stream {source_type} source {source_name}: {safe_error}", 

181 error_type=type(e).__name__, 

182 ) 

183 continue