Coverage for sources/librovore/exceptions.py: 65%

127 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-17 23:43 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Family of exceptions for package API. ''' 

22 

23 

24import urllib.parse as _urlparse 

25 

26from . import __ 

27 

28 

29class Omniexception( __.immut.Object, BaseException ): 

30 ''' Base for all exceptions raised by package API. ''' 

31 

32 _attribute_visibility_includes_: __.cabc.Collection[ str ] = ( 

33 frozenset( ( '__cause__', '__context__', ) ) ) 

34 

35 

36class Omnierror( Omniexception, Exception ): 

37 ''' Base for error exceptions raised by package API. ''' 

38 

39 

40class DetectionConfidenceInvalidity( Omnierror, ValueError ): 

41 ''' Detection confidence value is out of valid range. ''' 

42 

43 def __init__( self, confidence: float ): 

44 self.confidence = confidence 

45 super( ).__init__( f"Confidence {confidence} not in range [0.0, 1.0]" ) 

46 

47 

48class DocumentationContentAbsence( Omnierror, ValueError ): 

49 ''' Documentation main content container not found. ''' 

50 

51 def __init__( self, url: str ): 

52 message = f"No main content found in documentation at '{url}'." 

53 self.url = url 

54 super( ).__init__( message ) 

55 

56 

57class DocumentationInaccessibility( Omnierror, RuntimeError ): 

58 ''' Documentation file or resource absent or inaccessible. ''' 

59 

60 def __init__( self, url: str, cause: str | Exception ): 

61 message = f"Documentation at '{url}' is inaccessible. Cause: {cause}" 

62 self.url = url 

63 super( ).__init__( message ) 

64 

65 

66class DocumentationObjectAbsence( Omnierror, ValueError ): 

67 ''' Requested object not found in documentation page. ''' 

68 

69 def __init__( self, object_id: str, url: str ): 

70 message = ( 

71 f"Object '{object_id}' not found in documentation page " 

72 f"at '{url}'" ) 

73 self.object_id = object_id 

74 self.url = url 

75 super( ).__init__( message ) 

76 

77 

78class DocumentationParseFailure( Omnierror, ValueError ): 

79 ''' Documentation HTML parsing failed or content malformed. ''' 

80 

81 def __init__( self, url: str, cause: str | Exception ): 

82 message = f"Cannot parse documentation at '{url}'. Cause: {cause}" 

83 self.url = url 

84 super( ).__init__( message ) 

85 

86 

87class ExtensionCacheFailure( Omnierror, RuntimeError ): 

88 ''' Extension cache operation failed. ''' 

89 

90 def __init__( self, cache_path: __.Path, message: str ): 

91 self.cache_path = cache_path 

92 super( ).__init__( f"Cache error at '{cache_path}': {message}" ) 

93 

94 

95class ExtensionConfigurationInvalidity( Omnierror, ValueError ): 

96 ''' Extension configuration is invalid. ''' 

97 

98 def __init__( self, extension_name: str, message: str ): 

99 self.extension_name = extension_name 

100 super( ).__init__( f"Extension '{extension_name}': {message}" ) 

101 

102 

103class ExtensionInstallFailure( Omnierror, RuntimeError ): 

104 ''' Extension package installation failed. ''' 

105 

106 def __init__( self, package_spec: str, message: str ): 

107 self.package_spec = package_spec 

108 super( ).__init__( f"Failed to install '{package_spec}': {message}" ) 

109 

110 

111class ExtensionRegisterFailure( Omnierror, TypeError ): 

112 ''' Invalid plugin could not be registered. ''' 

113 

114 def __init__( self, message: str ): 

115 # TODO: Canned message with extension name as argument. 

116 super( ).__init__( message ) 

117 

118 

119class ExtensionVersionConflict( Omnierror, ImportError ): 

120 ''' Extension has incompatible version requirements. ''' 

121 

122 def __init__( self, package_name: str, required: str, available: str ): 

123 self.package_name = package_name 

124 self.required = required 

125 self.available = available 

126 super( ).__init__( 

127 f"Version conflict for '{package_name}': " 

128 f"required {required}, available {available}" ) 

129 

130 

131class HttpContentTypeInvalidity( Omnierror, ValueError ): 

132 ''' HTTP content type is not suitable for requested operation. ''' 

133 

134 def __init__( self, url: str, content_type: str, operation: str ): 

135 self.url = url 

136 self.content_type = content_type 

137 self.operation = operation 

138 super( ).__init__( 

139 f"Content type '{content_type}' not suitable for {operation} " 

140 f"operation on URL: {url}" ) 

141 

142 

143class InventoryFilterInvalidity( Omnierror, ValueError ): 

144 ''' Inventory filter is invalid. ''' 

145 

146 def __init__( self, message: str ): 

147 super( ).__init__( message ) 

148 

149 

150class InventoryInaccessibility( Omnierror, RuntimeError ): 

151 ''' Inventory file or resource absent or inaccessible. ''' 

152 

153 def __init__( self, source: str, cause: str | Exception ): 

154 message = f"Inventory at '{source}' is inaccessible. Cause: {cause}" 

155 self.source = source 

156 super( ).__init__( message ) 

157 

158 

159class InventoryInvalidity( Omnierror, ValueError ): 

160 ''' Inventory has invalid format or cannot be parsed. ''' 

161 

162 def __init__( self, source: str, cause: str | Exception ): 

163 message = f"Inventory at '{source}' is invalid. Cause: {cause}" 

164 self.source = source 

165 super( ).__init__( message ) 

166 

167 

168class InventoryUrlInvalidity( Omnierror, ValueError ): 

169 ''' Inventory URL is malformed or invalid. ''' 

170 

171 def __init__( self, source: str ): 

172 message = f"Invalid URL format: {source}" 

173 self.source = source 

174 super( ).__init__( message ) 

175 

176 

177class InventoryUrlNoSupport( Omnierror, NotImplementedError ): 

178 ''' Inventory URL has unsupported component. ''' 

179 

180 def __init__( 

181 self, url: _urlparse.ParseResult, component: str, 

182 value: __.Absential[ str ] = __.absent, 

183 ): 

184 url_s = _urlparse.urlunparse( url ) 

185 message_c = f"Component '{component}' " 

186 message_i = f"not supported in inventory URL '{url_s}'." 

187 message = ( 

188 f"{message_c} {message_i}" if __.is_absent( value ) 

189 else f"{message_c} with value '{value}' {message_i}" ) 

190 self.url = url 

191 super( ).__init__( message ) 

192 

193 

194class ProcessorGenusInvalidity( Omnierror, ValueError ): 

195 ''' Invalid processor genus provided. ''' 

196 

197 def __init__( self, genus: __.typx.Any ): 

198 message = f"Invalid ProcessorGenera: {genus}" 

199 self.genus = genus 

200 super( ).__init__( message ) 

201 

202 

203class ProcessorInavailability( Omnierror, RuntimeError ): 

204 ''' No processor found to handle source. ''' 

205 

206 def __init__( self, source: str ): 

207 message = f"No processor found to handle source: {source}" 

208 self.source = source 

209 super( ).__init__( message ) 

210 

211 

212class ProcessorInvalidity( Omnierror, TypeError ): 

213 ''' Processor has wrong type. ''' 

214 

215 def __init__( self, expected: str, actual: type ): 

216 message = f"Expected {expected}, got {actual}." 

217 self.expected_type = expected 

218 self.actual_type = actual 

219 super( ).__init__( message ) 

220 

221 

222class StructureIncompatibility( Omnierror, ValueError ): 

223 ''' Documentation structure incompatible with processor. ''' 

224 

225 def __init__( self, processor_name: str, source: str ): 

226 self.processor_name = processor_name 

227 self.source = source 

228 super( ).__init__( 

229 f"No content extracted by {processor_name} from {source}. " 

230 f"The documentation structure may be incompatible with " 

231 f"this processor." ) 

232 

233 

234class StructureProcessFailure( Omnierror, RuntimeError ): 

235 ''' Structure processor failed to complete processing. ''' 

236 

237 def __init__( self, processor_name: str, source: str, cause: str ): 

238 self.processor_name = processor_name 

239 self.source = source 

240 super( ).__init__( 

241 f"Processor {processor_name} failed processing {source}. " 

242 f"Cause: {cause}" ) 

243 

244 

245class ContentExtractFailure( StructureProcessFailure ): 

246 ''' Failed to extract meaningful content from documentation. ''' 

247 

248 def __init__( 

249 self, 

250 processor_name: str, 

251 source: str, 

252 meaningful_results: int, 

253 requested_objects: int, 

254 ): 

255 self.processor_name = processor_name 

256 self.source = source 

257 self.meaningful_results = meaningful_results 

258 self.requested_objects = requested_objects 

259 cause = ( 

260 f"Got {meaningful_results} meaningful results from " 

261 f"{requested_objects} requested objects. " 

262 f"This may indicate incompatible theme or documentation " 

263 f"structure." ) 

264 super( ).__init__( processor_name, source, cause ) 

265 

266 

267class ThemeDetectFailure( StructureProcessFailure ): 

268 ''' Theme detection failed during processing. ''' 

269 

270 def __init__( self, processor_name: str, source: str, theme_error: str ): 

271 self.theme_error = theme_error 

272 super( ).__init__( 

273 processor_name, source, f"Theme detection failed: {theme_error}" ) 

274 

275 

276class UrlImpermissibility( Omnierror, PermissionError ): 

277 ''' URL access blocked by robots.txt directive. ''' 

278 

279 def __init__( self, url: str, user_agent: str ): 

280 message = ( 

281 f"URL '{url}' blocked by robots.txt for " 

282 f"user agent '{user_agent}'" ) 

283 self.url = url 

284 self.user_agent = user_agent 

285 super( ).__init__( message )