1import functools 2from copy import deepcopy 3from .resource import Resource 4from .metadata import Metadata 5from .errors import Error, TaskError, ReportError 6from .exception import FrictionlessException 7from . import settings 8from . import helpers 9 10 11# NOTE: 12# We can allow some Report/ReportTask constructor kwargs be None 13# We need to review how we validate Report/ReportTask (strict mode is disabled) 14 15 16class Report(Metadata): 17 """Report representation. 18 19 API | Usage 20 -------- | -------- 21 Public | `from frictionless import Report` 22 23 Parameters: 24 descriptor? (str|dict): report descriptor 25 time (float): validation time 26 errors (Error[]): validation errors 27 tasks (ReportTask[]): validation tasks 28 29 Raises: 30 FrictionlessException: raise any error that occurs during the process 31 32 """ 33 34 def __init__(self, descriptor=None, *, time=None, errors=None, tasks=None): 35 36 # Store provided 37 self.setinitial("version", settings.VERSION) 38 self.setinitial("time", time) 39 self.setinitial("errors", errors) 40 self.setinitial("tasks", tasks) 41 super().__init__(descriptor) 42 43 # Store computed 44 error_count = len(self.errors) + sum(task.stats["errors"] for task in self.tasks) 45 self.setinitial("stats", {"errors": error_count, "tasks": len(self.tasks)}) 46 self.setinitial("valid", not error_count) 47 48 @property 49 def version(self): 50 """ 51 Returns: 52 str: frictionless version 53 """ 54 return self["version"] 55 56 @property 57 def time(self): 58 """ 59 Returns: 60 float: validation time 61 """ 62 return self["time"] 63 64 @property 65 def valid(self): 66 """ 67 Returns: 68 bool: validation result 69 """ 70 return self["valid"] 71 72 @property 73 def stats(self): 74 """ 75 Returns: 76 dict: validation stats 77 """ 78 return self["stats"] 79 80 @property 81 def errors(self): 82 """ 83 Returns: 84 Error[]: validation errors 85 """ 86 return self["errors"] 87 88 @property 89 def tasks(self): 90 """ 91 Returns: 92 ReportTask[]: validation tasks 93 """ 94 return self["tasks"] 95 96 @property 97 def task(self): 98 """ 99 Returns: 100 ReportTask: validation task (if there is only one) 101 102 Raises: 103 FrictionlessException: if there are more that 1 task 104 """ 105 if len(self.tasks) != 1: 106 error = Error(note='The "report.task" is available for single task reports') 107 raise FrictionlessException(error) 108 return self.tasks[0] 109 110 # Expand 111 112 def expand(self): 113 """Expand metadata""" 114 for task in self.tasks: 115 task.expand() 116 117 # Flatten 118 119 def flatten(self, spec=["taskPosition", "rowPosition", "fieldPosition", "code"]): 120 """Flatten the report 121 122 Parameters 123 spec (any[]): flatten specification 124 125 Returns: 126 any[]: flatten report 127 """ 128 result = [] 129 for error in self.errors: 130 context = {} 131 context.update(error) 132 result.append([context.get(prop) for prop in spec]) 133 for count, task in enumerate(self.tasks, start=1): 134 for error in task.errors: 135 context = {"taskNumber": count, "taskPosition": count} 136 context.update(error) 137 result.append([context.get(prop) for prop in spec]) 138 return result 139 140 # Import/Export 141 142 @staticmethod 143 def from_validate(validate): 144 """Validate function wrapper 145 146 Parameters: 147 validate (func): validate 148 149 Returns: 150 func: wrapped validate 151 """ 152 153 @functools.wraps(validate) 154 def wrapper(*args, **kwargs): 155 timer = helpers.Timer() 156 try: 157 return validate(*args, **kwargs) 158 except Exception as exception: 159 error = TaskError(note=str(exception)) 160 if isinstance(exception, FrictionlessException): 161 error = exception.error 162 return Report(time=timer.time, errors=[error], tasks=[]) 163 164 return wrapper 165 166 # Metadata 167 168 metadata_Error = ReportError 169 metadata_profile = deepcopy(settings.REPORT_PROFILE) 170 metadata_profile["properties"]["tasks"] = {"type": "array"} 171 172 def metadata_process(self): 173 174 # Tasks 175 tasks = self.get("tasks") 176 if isinstance(tasks, list): 177 for index, task in enumerate(tasks): 178 if not isinstance(task, ReportTask): 179 task = ReportTask(task) 180 list.__setitem__(tasks, index, task) 181 if not isinstance(tasks, helpers.ControlledList): 182 tasks = helpers.ControlledList(tasks) 183 tasks.__onchange__(self.metadata_process) 184 dict.__setitem__(self, "tasks", tasks) 185 186 def metadata_validate(self): 187 yield from super().metadata_validate() 188 189 # Tasks 190 for task in self.tasks: 191 yield from task.metadata_errors 192 193 194class ReportTask(Metadata): 195 """Report task representation. 196 197 API | Usage 198 -------- | -------- 199 Public | `from frictionless import ReportTask` 200 201 Parameters: 202 descriptor? (str|dict): schema descriptor 203 time (float): validation time 204 scope (str[]): validation scope 205 partial (bool): wehter validation was partial 206 errors (Error[]): validation errors 207 task (Task): validation task 208 209 # Raises 210 FrictionlessException: raise any error that occurs during the process 211 212 """ 213 214 def __init__( 215 self, 216 descriptor=None, 217 *, 218 resource=None, 219 time=None, 220 scope=None, 221 partial=None, 222 errors=None 223 ): 224 225 # Store provided 226 self.setinitial("resource", resource) 227 self.setinitial("time", time) 228 self.setinitial("scope", scope) 229 self.setinitial("partial", partial) 230 self.setinitial("errors", errors) 231 super().__init__(descriptor) 232 233 # Store computed 234 self.setinitial("stats", {"errors": len(self.errors)}) 235 self.setinitial("valid", not self.errors) 236 237 @property 238 def resource(self): 239 """ 240 Returns: 241 Resource: resource 242 """ 243 return self["resource"] 244 245 @property 246 def time(self): 247 """ 248 Returns: 249 float: validation time 250 """ 251 return self["time"] 252 253 @property 254 def valid(self): 255 """ 256 Returns: 257 bool: validation result 258 """ 259 return self["valid"] 260 261 @property 262 def scope(self): 263 """ 264 Returns: 265 str[]: validation scope 266 """ 267 return self["scope"] 268 269 @property 270 def partial(self): 271 """ 272 Returns: 273 bool: if validation partial 274 """ 275 return self["partial"] 276 277 @property 278 def stats(self): 279 """ 280 Returns: 281 dict: validation stats 282 """ 283 return self["stats"] 284 285 @property 286 def errors(self): 287 """ 288 Returns: 289 Error[]: validation errors 290 """ 291 return self["errors"] 292 293 @property 294 def error(self): 295 """ 296 Returns: 297 Error: validation error if there is only one 298 299 Raises: 300 FrictionlessException: if more than one errors 301 """ 302 if len(self.errors) != 1: 303 error = Error(note='The "task.error" is available for single error tasks') 304 raise FrictionlessException(error) 305 return self.errors[0] 306 307 # Expand 308 309 def expand(self): 310 """Expand metadata""" 311 self.resource.expand() 312 313 # Flatten 314 315 def flatten(self, spec=["rowPosition", "fieldPosition", "code"]): 316 """Flatten the report 317 318 Parameters 319 spec (any[]): flatten specification 320 321 Returns: 322 any[]: flatten task report 323 """ 324 result = [] 325 for error in self.errors: 326 context = {} 327 context.update(error) 328 result.append([context.get(prop) for prop in spec]) 329 return result 330 331 # Metadata 332 333 metadata_Error = ReportError 334 metadata_profile = settings.REPORT_PROFILE["properties"]["tasks"]["items"] 335 336 def metadata_process(self): 337 338 # Resource 339 resource = self.get("resource") 340 if not isinstance(resource, Resource): 341 resource = Resource(resource) 342 dict.__setitem__(self, "resource", resource) 343