robotframework-xmlvalidator 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ # Copyright 2024-2025 Michael Hallik
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ #
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied.
14
+ #
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+
19
+ """
20
+ This module serves as the package entry point, by exposing the
21
+ XmlValidator class directly at the package level.
22
+
23
+ It further provides versioning metadata for use in documentation and
24
+ automation.
25
+
26
+ Features:
27
+
28
+ - Defines the public API (__all__, aliasing).
29
+ - Simplifies imports by exposing XmlValidator at the top level.
30
+ - Supports dynamic version retrieval/discovery from installed metadata.
31
+ - Maintains compatibility with Python < 3.8 via fallback imports.
32
+ """
33
+
34
+
35
+ # pylint: disable=C0103:invalid-name # On account of the class name, which is not snake-cased (required by RF).
36
+
37
+
38
+ # Import versioning utilities to fetch package metadata dynamically.
39
+ # - `version` retrieves the installed package version from metadata.
40
+ # - `PackageNotFoundError` handles cases where the package is not installed.
41
+ from importlib.metadata import version, PackageNotFoundError
42
+ # Expose XmlValidator directly for cleaner imports.
43
+ from .XmlValidator import XmlValidator
44
+
45
+
46
+ # Alias (i.e.: make available) XmlValidator at the package level for RF.
47
+ xmlvalidator = XmlValidator
48
+
49
+ # Define package metadata & control what is exposed when importing the package.
50
+ __all__ = ["XmlValidator"] # Controls what's exposed by: from package import *.
51
+ __author__ = "Michael Hallik"
52
+ # Expose the version (package version or fallback version).
53
+ try:
54
+ # Fetches version when installed as package.
55
+ __version__ = version("robotframework-xmlvalidator")
56
+ except PackageNotFoundError:
57
+ # Fall back when package not installed (default version for development).
58
+ __version__ = "0.0.1"
59
+ import warnings
60
+ warnings.warn("Package metadata not found, using fallback version.")
@@ -0,0 +1,413 @@
1
+ # Copyright 2024-2025 Michael Hallik
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ #
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ # implied.
14
+ #
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+
19
+ """
20
+ Provides result-handling components for XML validation operations.
21
+
22
+ This module defines:
23
+
24
+ - ValidatorResultRecorder:
25
+ Encapsulates validation results, including valid/invalid files and
26
+ associated error details. Supports logging and also exporting to CSV.
27
+ - ValidatorResult:
28
+ A lightweight result wrapper for encapsulating success/failure states
29
+ and their corresponding values or errors.
30
+
31
+ These classes are used internally by the XmlValidator library and
32
+ associated utilities to manage and report outcomes of validation
33
+ tasks.
34
+ """
35
+
36
+
37
+ # Standard library imports.
38
+ from dataclasses import dataclass, field
39
+ from datetime import datetime
40
+ from pathlib import Path
41
+ from typing import Any, Dict, List, Optional
42
+ # Third party library imports.
43
+ import pandas as pd
44
+ from robot.api import logger
45
+
46
+
47
+ # This class is structured as a dataclass for default field setup, but
48
+ # (also) provides utility methods for result recording and reporting.
49
+ @dataclass
50
+ class ValidatorResultRecorder:
51
+ """
52
+ Collects and manages results from XML validation runs.
53
+
54
+ This class serves as an internal aggregator for storing validation
55
+ outcomes, including:
56
+
57
+ - A summary of valid and invalid XML files.
58
+ - Detailed validation errors grouped by file.
59
+
60
+ It supports logging of individual errors and summary statistics to
61
+ the Robot Framework log, as well as exporting all errors to a
62
+ CSV file.
63
+
64
+ Attributes:
65
+
66
+ - errors_by_file (List[Dict[str, Any]]):
67
+ A list of validation error dictionaries, each tagged with its
68
+ corresponding file name.
69
+
70
+ - validation_summary (Dict[str, List[str]]):
71
+ A dictionary with two keys: 'valid' and 'invalid'. Each key maps
72
+ to a list of file names.
73
+
74
+ This class is used internally by XmlValidator to record, summarize,
75
+ and export validation results.
76
+ """
77
+
78
+ __version__ = '1.0.0'
79
+
80
+ errors_by_file: List[Dict[str, Any]] = field(default_factory=list)
81
+ validation_summary: Dict[str, List[str]] = field(
82
+ default_factory=lambda: {"valid": [], "invalid": []}
83
+ )
84
+
85
+ def _get_summary(self) -> Dict[str, int]:
86
+ """
87
+ Constructs a summary of validation outcomes.
88
+
89
+ This internal method returns a dictionary containing the count
90
+ of validated, valid, and invalid files. It safely handles cases
91
+ where either category is missing from the summary.
92
+
93
+ Returns:
94
+
95
+ - Dict[str, int]:
96
+ {
97
+ "Total_files validated": int,
98
+ "Valid files": int,
99
+ "Invalid files": int
100
+ }
101
+ """
102
+ valid_files = len(self.validation_summary.get("valid", []))
103
+ invalid_files = len(self.validation_summary.get("invalid", []))
104
+
105
+ return {
106
+ "Total_files validated": valid_files + invalid_files,
107
+ "Valid files": valid_files,
108
+ "Invalid files": invalid_files,
109
+ }
110
+
111
+ def add_file_errors(
112
+ self,
113
+ file_path: Path,
114
+ error_details: List[Dict[str, Any]] | Dict[str, Any] | None
115
+ ) -> None:
116
+ """
117
+ Adds validation error(s) for a given XML file.
118
+
119
+ Accepts either a single error dictionary or a list of error
120
+ dictionaries and appends them to the `errors_by_file` attribute.
121
+ Each error entry is tagged with the file name.
122
+
123
+ Args:
124
+
125
+ - file_path (Path):
126
+ The path of the XML file that caused the error(s).
127
+
128
+ - error_details (Dict or List[Dict] or None):
129
+ The validation error(s) to record. If a single dictionary is
130
+ provided, it is internally converted to a list.
131
+
132
+ Returns:
133
+
134
+ None
135
+ """
136
+ if error_details:
137
+ # Normalize error_details to always be a list.
138
+ if isinstance(error_details, dict):
139
+ error_details = [error_details]
140
+ # Append each error to the errors_by_file list.
141
+ for error in error_details:
142
+ error_entry = {"file_name": file_path.name, **error}
143
+ self.errors_by_file.append(error_entry)
144
+
145
+ def add_invalid_file(
146
+ self,
147
+ file_path: Path
148
+ ) -> None:
149
+ """
150
+ Records a file as invalid and logs the result.
151
+
152
+ Adds the file name to the `invalid` list within the
153
+ `validation_summary` attribute and emits a warning log
154
+ to Robot Framework output.
155
+
156
+ Args:
157
+
158
+ - file_path (Path):
159
+ The path to the XML file that failed validation.
160
+
161
+ Returns:
162
+
163
+ None
164
+ """
165
+ logger.warn("\tXML is invalid:")
166
+ self.validation_summary["invalid"].append(file_path.name)
167
+
168
+ def add_valid_file(
169
+ self,
170
+ file_path: Path
171
+ ) -> None:
172
+ """
173
+ Records a file as valid and logs the result.
174
+
175
+ Adds the file name to the `valid` list within the
176
+ `validation_summary` attribute and emits a confirmation
177
+ log to the Robot Framework output.
178
+
179
+ Args:
180
+
181
+ - file_path (Path):
182
+ The path to the XML file that passed validation.
183
+
184
+ Returns:
185
+
186
+ None
187
+ """
188
+ logger.info("\tXML is valid!", also_console=True)
189
+ self.validation_summary["valid"].append(file_path.name)
190
+
191
+ def log_file_errors(
192
+ self,
193
+ errors: List[Dict[str, Any]]
194
+ ) -> None:
195
+ """
196
+ Logs a list of validation errors to the Robot Framework log.
197
+
198
+ Each error dictionary is logged under a numbered header
199
+ (e.g., "Error #1") followed by its individual key-value
200
+ pairs.
201
+
202
+ Args:
203
+
204
+ - errors (List[Dict[str, Any]]):
205
+ A list of dictionaries containing validation error details for
206
+ one or more XML files.
207
+
208
+ Returns:
209
+
210
+ None
211
+ """
212
+ for idx, error in enumerate(errors):
213
+ logger.warn(f'\t\tError #{idx}:')
214
+ for key, value in error.items():
215
+ logger.warn(f"\t\t\t{key}: {value}")
216
+
217
+ def log_summary(self) -> None:
218
+ """
219
+ Logs a summary of validation results to the Robot Framework log.
220
+
221
+ This method retrieves the number of valid, invalid, and total
222
+ files from `_get_summary()` and prints them in a structured
223
+ format.
224
+
225
+ Returns:
226
+
227
+ None
228
+ """
229
+ for category, value in self._get_summary().items():
230
+ logger.info(f"{category}: {value}.", also_console=True)
231
+
232
+ def reset(self) -> None:
233
+ """
234
+ Clears all stored validation results.
235
+
236
+ This method resets the internal state of the result recorder,
237
+ including:
238
+
239
+ - `errors_by_file`:
240
+ cleared
241
+ - `validation_summary`:
242
+ reset to default structure with empty 'valid' and 'invalid'
243
+ lists
244
+
245
+ Call this method before starting a new validation run if you
246
+ want to discard previous results.
247
+
248
+ Returns:
249
+
250
+ None
251
+ """
252
+ self.errors_by_file.clear()
253
+ self.validation_summary = {"valid": [], "invalid": []}
254
+
255
+ def write_errors_to_csv(self,
256
+ errors: List[ Dict[str, Any] ],
257
+ output_path: Path,
258
+ include_timestamp: Optional[bool] = False,
259
+ file_name_column: Optional[str] = None
260
+ ) -> str:
261
+ """
262
+ Writes a list of validation errors to a CSV file.
263
+
264
+ This method takes a list of error dictionaries and writes them
265
+ to a CSV file at the specified output path. Optionally, it
266
+ appends a timestamp to the file name and reorders columns to
267
+ place a specific column (e.g., "file_name") first.
268
+
269
+ Args:
270
+
271
+ - errors (List[Dict[str, Any]]):
272
+ A list of dictionaries, where each dictionary contains details
273
+ of a validation error. Each key in the dictionaries
274
+ corresponds to a column in the output CSV.
275
+
276
+ - output_path (Path):
277
+ The base path for the output CSV file. A timestamp will be
278
+ appended to the filename if `include_timestamp` is True.
279
+
280
+ - include_timestamp (bool, optional):
281
+ If True, appends a timestamp (in the format
282
+ `YYYY-MM-DD_HH-MM-SS`) to the output file name. Defaults to
283
+ False.
284
+
285
+ - file_name_column (str, optional):
286
+ The name of the column to be placed first in the CSV. If the
287
+ specified column is not present, the original column order is
288
+ preserved.
289
+
290
+ Raises:
291
+
292
+ - ValueError:
293
+ Raised if the `errors` list is empty or improperly formatted.
294
+
295
+ - IOError:
296
+ Raised if writing the CSV fails due to file system issues.
297
+
298
+ Returns:
299
+
300
+ - str:
301
+ The resolved path of the created CSV file.
302
+
303
+ Notes:
304
+
305
+ - If `errors` is an empty list, the method exits early and logs
306
+ an informational message without creating a file.
307
+ - Column reordering occurs only if `file_name_column` exists in
308
+ the error dictionaries.
309
+ - The method uses `pandas` for CSV generation.
310
+ """
311
+ # Return if no errors were passed.
312
+ if not errors:
313
+ logger.info("No errors to write to CSV.")
314
+ return ''
315
+ # Generate a timestamp to be added to the filename.
316
+ timestamp = f'_{datetime.now().strftime("%Y-%m-%d_%H-%M-%S-%f")}' \
317
+ if include_timestamp else None
318
+ # Construct the output path.
319
+ output_csv_path = (
320
+ output_path.parent / f"errors{timestamp if timestamp else ''}.csv"
321
+ )
322
+ # Convert the errors list to a DataFrame.
323
+ df = pd.DataFrame(errors)
324
+ # Ensure the specified column is first, if provided.
325
+ if file_name_column and file_name_column in df.columns:
326
+ columns_order = [file_name_column] + [
327
+ col for col in df.columns if col != file_name_column
328
+ ]
329
+ df = df[columns_order]
330
+ # Write the DataFrame to a CSV file.
331
+ try:
332
+ df.to_csv(output_csv_path, index=False)
333
+ logger.info(
334
+ f"Validation errors exported to '{output_csv_path}'.",
335
+ also_console=True
336
+ )
337
+ except IOError as e:
338
+ raise IOError(
339
+ f"Failed to write CSV file: {output_csv_path}."
340
+ ) from e
341
+ return str( output_csv_path.resolve() )
342
+
343
+ class ValidatorResult: # pylint: disable=R0903:too-few-public-methods
344
+ """
345
+ Encapsulates the result of an operation in a success-or-failure format.
346
+
347
+ `ValidatorResult` provides a structured way to handle the outcome of
348
+ operations throughout the XML validation library. It captures whether
349
+ an operation succeeded and includes either the result (`value`) or
350
+ the error (`error`) — but not both.
351
+
352
+ This pattern allows methods to return a single object regardless of
353
+ success or failure, simplifying error handling.
354
+
355
+ Attributes:
356
+
357
+ - success (bool):
358
+ True if the operation was successful; False otherwise.
359
+
360
+ - value (Any, optional):
361
+ The returned data from a successful operation.
362
+
363
+ - error (Any, optional):
364
+ Error information if the operation failed.
365
+ """
366
+
367
+ __version__ = '0.0.1'
368
+
369
+ def __init__(
370
+ self,
371
+ success: bool,
372
+ value: Optional[Any] = None,
373
+ error: Optional[Any] = None
374
+ ) -> None:
375
+ """
376
+ Initializes a ValidatorResult instance.
377
+
378
+ Used to encapsulate the outcome of an operation, including
379
+ success state, result value, or error information.
380
+
381
+ Args:
382
+
383
+ - success (bool):
384
+ Whether the operation was successful.
385
+ - value (Any, optional):
386
+ The result of the operation, if successful. Defaults to None.
387
+ - error (Any, optional):
388
+ Error details if the operation failed. Defaults to None.
389
+
390
+ Returns:
391
+
392
+ None
393
+ """
394
+ self.success = success
395
+ self.value = value
396
+ self.error = error
397
+
398
+ def __repr__(self) -> str:
399
+ """
400
+ Returns a string representation of the ValidatorResult instance.
401
+
402
+ If the result is successful, includes the value.
403
+
404
+ Otherwise, includes the error details.
405
+
406
+ Returns:
407
+
408
+ str
409
+ """
410
+ if self.success: # pylint: disable=R1705:no-else-return
411
+ return f"Result(success=True, value={self.value})"
412
+ else:
413
+ return f"Result(success=False, error={self.error})"