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