rdetoolkit 1.5.1__cp310-cp310-win_amd64.whl → 1.5.3__cp310-cp310-win_amd64.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.
- rdetoolkit/__init__.py +19 -1
- rdetoolkit/cli/validate.py +84 -55
- rdetoolkit/cmd/validate.py +18 -15
- rdetoolkit/config.py +211 -23
- rdetoolkit/core.cp310-win_amd64.pyd +0 -0
- rdetoolkit/exceptions.py +94 -0
- rdetoolkit/exceptions.pyi +21 -0
- rdetoolkit/invoicefile.py +11 -9
- rdetoolkit/models/metadata.py +97 -2
- rdetoolkit/models/metadata.pyi +22 -1
- rdetoolkit/processing/processors/invoice.py +41 -0
- rdetoolkit/validation.py +94 -8
- rdetoolkit/validation.pyi +5 -0
- rdetoolkit/workflows.py +8 -2
- {rdetoolkit-1.5.1.dist-info → rdetoolkit-1.5.3.dist-info}/METADATA +17 -9
- {rdetoolkit-1.5.1.dist-info → rdetoolkit-1.5.3.dist-info}/RECORD +19 -19
- {rdetoolkit-1.5.1.dist-info → rdetoolkit-1.5.3.dist-info}/WHEEL +0 -0
- {rdetoolkit-1.5.1.dist-info → rdetoolkit-1.5.3.dist-info}/entry_points.txt +0 -0
- {rdetoolkit-1.5.1.dist-info → rdetoolkit-1.5.3.dist-info}/licenses/LICENSE +0 -0
rdetoolkit/__init__.py
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
# Python 3.9 deprecation warning (Issue #360)
|
|
7
|
+
if sys.version_info < (3, 10):
|
|
8
|
+
warnings.warn(
|
|
9
|
+
"Python 3.9 support is deprecated and will be removed in rdetoolkit v2.0. "
|
|
10
|
+
"Please upgrade to Python 3.10 or later.",
|
|
11
|
+
DeprecationWarning,
|
|
12
|
+
stacklevel=2,
|
|
13
|
+
)
|
|
14
|
+
|
|
3
15
|
from importlib import import_module
|
|
4
16
|
from typing import Any
|
|
5
17
|
|
|
6
|
-
__version__ = "1.5.
|
|
18
|
+
__version__ = "1.5.3"
|
|
7
19
|
|
|
8
20
|
_LAZY_ATTRS: dict[str, tuple[str, str]] = {
|
|
9
21
|
"DirectoryOps": ("rdetoolkit.core", "DirectoryOps"),
|
|
@@ -26,6 +38,9 @@ _LAZY_MODULES: dict[str, str] = {
|
|
|
26
38
|
"rde2util": "rdetoolkit.rde2util",
|
|
27
39
|
"rdelogger": "rdetoolkit.rdelogger",
|
|
28
40
|
"workflows": "rdetoolkit.workflows",
|
|
41
|
+
"processing": "rdetoolkit.processing",
|
|
42
|
+
"storage": "rdetoolkit.storage",
|
|
43
|
+
"traceback": "rdetoolkit.traceback",
|
|
29
44
|
"config": "rdetoolkit.models.config",
|
|
30
45
|
"invoice": "rdetoolkit.models.invoice",
|
|
31
46
|
"invoice_schema": "rdetoolkit.models.invoice_schema",
|
|
@@ -55,6 +70,9 @@ __all__ = [
|
|
|
55
70
|
"rde2util",
|
|
56
71
|
"rdelogger",
|
|
57
72
|
"workflows",
|
|
73
|
+
"processing",
|
|
74
|
+
"storage",
|
|
75
|
+
"traceback",
|
|
58
76
|
"config",
|
|
59
77
|
"invoice",
|
|
60
78
|
"invoice_schema",
|
rdetoolkit/cli/validate.py
CHANGED
|
@@ -13,7 +13,13 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
app = typer.Typer(
|
|
15
15
|
name="validate",
|
|
16
|
-
help="Validate RDE schema and data files
|
|
16
|
+
help="""Validate RDE schema and data files.
|
|
17
|
+
|
|
18
|
+
All validate commands use standardized exit codes:
|
|
19
|
+
0 = Success (validation passed)
|
|
20
|
+
1 = Validation failure (data/schema issues)
|
|
21
|
+
2 = Usage error (invalid arguments, missing files)
|
|
22
|
+
""",
|
|
17
23
|
no_args_is_help=True,
|
|
18
24
|
)
|
|
19
25
|
|
|
@@ -87,10 +93,11 @@ def handle_validation_error(error: Exception, error_type: str) -> None:
|
|
|
87
93
|
typer.echo(str(error), err=True)
|
|
88
94
|
raise typer.Exit(code=1) from error
|
|
89
95
|
|
|
90
|
-
#
|
|
91
|
-
|
|
96
|
+
# All other errors (including internal errors) map to exit code 2
|
|
97
|
+
# This includes unexpected exceptions that indicate configuration or usage issues
|
|
98
|
+
msg = f"Error during {error_type}: {error}"
|
|
92
99
|
typer.echo(msg, err=True)
|
|
93
|
-
raise typer.Exit(code=
|
|
100
|
+
raise typer.Exit(code=2) from error
|
|
94
101
|
|
|
95
102
|
|
|
96
103
|
# Common options for validation commands
|
|
@@ -141,6 +148,11 @@ def invoice_schema(
|
|
|
141
148
|
Validates that the invoice schema file conforms to the expected structure
|
|
142
149
|
and contains valid field definitions.
|
|
143
150
|
|
|
151
|
+
Exit Codes:
|
|
152
|
+
0: Success - validation passed
|
|
153
|
+
1: Validation failure - schema structure issues found
|
|
154
|
+
2: Usage error - file not found or invalid arguments
|
|
155
|
+
|
|
144
156
|
Examples:
|
|
145
157
|
rdetoolkit validate invoice-schema tasksupport/invoice.schema.json
|
|
146
158
|
rdetoolkit validate invoice-schema schema.json --format json
|
|
@@ -151,6 +163,9 @@ def invoice_schema(
|
|
|
151
163
|
command = InvoiceSchemaCommand(schema_path)
|
|
152
164
|
result = command.execute()
|
|
153
165
|
handle_validation_result(result, format_type, strict, quiet)
|
|
166
|
+
except typer.Exit:
|
|
167
|
+
# Re-raise typer.Exit to let Typer handle it properly
|
|
168
|
+
raise
|
|
154
169
|
except FileNotFoundError:
|
|
155
170
|
handle_file_not_found(schema_path, "Schema")
|
|
156
171
|
except Exception as e:
|
|
@@ -177,6 +192,11 @@ def metadata_def(
|
|
|
177
192
|
Validates that the metadata definition file conforms to the expected
|
|
178
193
|
structure defined by the MetadataItem schema.
|
|
179
194
|
|
|
195
|
+
Exit Codes:
|
|
196
|
+
0: Success - validation passed
|
|
197
|
+
1: Validation failure - definition structure issues found
|
|
198
|
+
2: Usage error - file not found or invalid arguments
|
|
199
|
+
|
|
180
200
|
Examples:
|
|
181
201
|
rdetoolkit validate metadata-def tasksupport/metadata_def.json
|
|
182
202
|
rdetoolkit validate metadata-def metadata_def.json --format json
|
|
@@ -187,6 +207,9 @@ def metadata_def(
|
|
|
187
207
|
command = MetadataDefCommand(metadata_def_path)
|
|
188
208
|
result = command.execute()
|
|
189
209
|
handle_validation_result(result, format_type, strict, quiet)
|
|
210
|
+
except typer.Exit:
|
|
211
|
+
# Re-raise typer.Exit to let Typer handle it properly
|
|
212
|
+
raise
|
|
190
213
|
except FileNotFoundError:
|
|
191
214
|
handle_file_not_found(metadata_def_path, "Metadata definition")
|
|
192
215
|
except Exception as e:
|
|
@@ -195,26 +218,22 @@ def metadata_def(
|
|
|
195
218
|
|
|
196
219
|
@app.command("invoice")
|
|
197
220
|
def invoice(
|
|
198
|
-
invoice_path:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
dir_okay=False,
|
|
215
|
-
resolve_path=True,
|
|
216
|
-
),
|
|
217
|
-
],
|
|
221
|
+
invoice_path: Path = typer.Argument(
|
|
222
|
+
...,
|
|
223
|
+
help="Path to invoice.json file",
|
|
224
|
+
exists=True,
|
|
225
|
+
dir_okay=False,
|
|
226
|
+
resolve_path=True,
|
|
227
|
+
),
|
|
228
|
+
schema_path: Path = typer.Option(
|
|
229
|
+
...,
|
|
230
|
+
"--schema",
|
|
231
|
+
"-s",
|
|
232
|
+
help="Path to invoice.schema.json file",
|
|
233
|
+
exists=True,
|
|
234
|
+
dir_okay=False,
|
|
235
|
+
resolve_path=True,
|
|
236
|
+
),
|
|
218
237
|
format_type: FormatOption = OutputFormat.TEXT,
|
|
219
238
|
strict: StrictOption = False,
|
|
220
239
|
quiet: QuietOption = False,
|
|
@@ -224,6 +243,11 @@ def invoice(
|
|
|
224
243
|
Validates that the invoice.json file conforms to the structure and
|
|
225
244
|
constraints defined in the invoice.schema.json file.
|
|
226
245
|
|
|
246
|
+
Exit Codes:
|
|
247
|
+
0: Success - validation passed
|
|
248
|
+
1: Validation failure - data violates schema constraints
|
|
249
|
+
2: Usage error - files not found or invalid arguments
|
|
250
|
+
|
|
227
251
|
Examples:
|
|
228
252
|
rdetoolkit validate invoice raw/invoice.json --schema tasksupport/invoice.schema.json
|
|
229
253
|
rdetoolkit validate invoice invoice.json -s schema.json --format json
|
|
@@ -251,26 +275,22 @@ def invoice(
|
|
|
251
275
|
|
|
252
276
|
@app.command("metadata")
|
|
253
277
|
def metadata(
|
|
254
|
-
metadata_path:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
dir_okay=False,
|
|
271
|
-
resolve_path=True,
|
|
272
|
-
),
|
|
273
|
-
],
|
|
278
|
+
metadata_path: Path = typer.Argument(
|
|
279
|
+
...,
|
|
280
|
+
help="Path to metadata.json file",
|
|
281
|
+
exists=True,
|
|
282
|
+
dir_okay=False,
|
|
283
|
+
resolve_path=True,
|
|
284
|
+
),
|
|
285
|
+
schema_path: Path = typer.Option(
|
|
286
|
+
...,
|
|
287
|
+
"--schema",
|
|
288
|
+
"-s",
|
|
289
|
+
help="Path to metadata definition JSON file",
|
|
290
|
+
exists=True,
|
|
291
|
+
dir_okay=False,
|
|
292
|
+
resolve_path=True,
|
|
293
|
+
),
|
|
274
294
|
format_type: FormatOption = OutputFormat.TEXT,
|
|
275
295
|
strict: StrictOption = False,
|
|
276
296
|
quiet: QuietOption = False,
|
|
@@ -280,6 +300,11 @@ def metadata(
|
|
|
280
300
|
Validates that the metadata.json file conforms to the structure
|
|
281
301
|
defined in the metadata definition file.
|
|
282
302
|
|
|
303
|
+
Exit Codes:
|
|
304
|
+
0: Success - validation passed
|
|
305
|
+
1: Validation failure - data violates definition constraints
|
|
306
|
+
2: Usage error - files not found or invalid arguments
|
|
307
|
+
|
|
283
308
|
Examples:
|
|
284
309
|
rdetoolkit validate metadata raw/metadata.json --schema tasksupport/metadata_def.json
|
|
285
310
|
rdetoolkit validate metadata metadata.json -s metadata_def.json --format json
|
|
@@ -290,6 +315,9 @@ def metadata(
|
|
|
290
315
|
command = MetadataCommand(metadata_path, schema_path)
|
|
291
316
|
result = command.execute()
|
|
292
317
|
handle_validation_result(result, format_type, strict, quiet)
|
|
318
|
+
except typer.Exit:
|
|
319
|
+
# Re-raise typer.Exit to let Typer handle it properly
|
|
320
|
+
raise
|
|
293
321
|
except FileNotFoundError:
|
|
294
322
|
# Determine which file is missing
|
|
295
323
|
if not metadata_path.exists():
|
|
@@ -304,16 +332,14 @@ def metadata(
|
|
|
304
332
|
|
|
305
333
|
@app.command("all")
|
|
306
334
|
def validate_all(
|
|
307
|
-
project_dir:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
),
|
|
316
|
-
] = None,
|
|
335
|
+
project_dir: Optional[Path] = typer.Argument(
|
|
336
|
+
None,
|
|
337
|
+
help="Root directory of RDE project (defaults to current directory)",
|
|
338
|
+
exists=True,
|
|
339
|
+
file_okay=False,
|
|
340
|
+
dir_okay=True,
|
|
341
|
+
resolve_path=True,
|
|
342
|
+
),
|
|
317
343
|
format_type: FormatOption = OutputFormat.TEXT,
|
|
318
344
|
strict: StrictOption = False,
|
|
319
345
|
quiet: QuietOption = False,
|
|
@@ -329,7 +355,10 @@ def validate_all(
|
|
|
329
355
|
- input/invoice/invoice.json (with schema)
|
|
330
356
|
- input/metadata/metadata.json (if exists, with schema)
|
|
331
357
|
|
|
332
|
-
Exit
|
|
358
|
+
Exit Codes:
|
|
359
|
+
0: Success - all validations passed
|
|
360
|
+
1: Validation failure - one or more validations failed
|
|
361
|
+
2: Usage error - project directory not found or invalid arguments
|
|
333
362
|
|
|
334
363
|
Examples:
|
|
335
364
|
rdetoolkit validate all
|
rdetoolkit/cmd/validate.py
CHANGED
|
@@ -8,7 +8,7 @@ from dataclasses import dataclass
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Optional, Union
|
|
10
10
|
|
|
11
|
-
from rdetoolkit.validation import InvoiceValidator, MetadataValidator
|
|
11
|
+
from rdetoolkit.validation import InvoiceValidator, MetadataDefinitionValidator, MetadataValidator
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass
|
|
@@ -181,8 +181,7 @@ def determine_exit_code(result: ValidationResult, strict: bool = False) -> int:
|
|
|
181
181
|
Exit codes:
|
|
182
182
|
- 0: Validation passed
|
|
183
183
|
- 1: Validation failed (errors or warnings in strict mode)
|
|
184
|
-
- 2: Usage/argument errors (handled by
|
|
185
|
-
- 3: Internal errors (handled by exception handlers)
|
|
184
|
+
- 2: Usage/argument errors (handled by CLI layer)
|
|
186
185
|
|
|
187
186
|
Args:
|
|
188
187
|
result: Validation result
|
|
@@ -428,7 +427,11 @@ class MetadataDefCommand(_ValidationErrorParser):
|
|
|
428
427
|
"""Command to validate metadata definition JSON files.
|
|
429
428
|
|
|
430
429
|
This command validates metadata definition files using the
|
|
431
|
-
|
|
430
|
+
MetadataDefinitionValidator (for metadata-def.json structure).
|
|
431
|
+
|
|
432
|
+
Note:
|
|
433
|
+
This is separate from MetadataCommand which validates metadata.json
|
|
434
|
+
data files against metadata definition schemas.
|
|
432
435
|
"""
|
|
433
436
|
|
|
434
437
|
def __init__(self, metadata_def_path: str | Path) -> None:
|
|
@@ -460,7 +463,8 @@ class MetadataDefCommand(_ValidationErrorParser):
|
|
|
460
463
|
warnings: list[ValidationWarning] = []
|
|
461
464
|
|
|
462
465
|
try:
|
|
463
|
-
|
|
466
|
+
# Use MetadataDefinitionValidator for metadata-def.json
|
|
467
|
+
validator = MetadataDefinitionValidator()
|
|
464
468
|
# validate() with path parameter validates the file
|
|
465
469
|
_ = validator.validate(path=self.metadata_def_path)
|
|
466
470
|
|
|
@@ -520,16 +524,15 @@ class MetadataCommand(_ValidationErrorParser):
|
|
|
520
524
|
warnings: list[ValidationWarning] = []
|
|
521
525
|
|
|
522
526
|
try:
|
|
523
|
-
# First validate the schema/definition
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
#
|
|
529
|
-
#
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
_ = validator.validate(path=self.metadata_path)
|
|
527
|
+
# First validate the schema/definition file (metadata-def.json)
|
|
528
|
+
# Uses MetadataDefinitionValidator for dict[str, MetadataDefEntry] structure
|
|
529
|
+
def_validator = MetadataDefinitionValidator()
|
|
530
|
+
_ = def_validator.validate(path=self.schema_path)
|
|
531
|
+
|
|
532
|
+
# Then validate the metadata data file (metadata.json)
|
|
533
|
+
# Uses MetadataValidator for MetadataItem structure (constant/variable)
|
|
534
|
+
data_validator = MetadataValidator()
|
|
535
|
+
_ = data_validator.validate(path=self.metadata_path)
|
|
533
536
|
|
|
534
537
|
except Exception as e:
|
|
535
538
|
# Parse validation errors from exception message
|
rdetoolkit/config.py
CHANGED
|
@@ -6,8 +6,11 @@ from typing import Any, Final
|
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
8
|
from pydantic import ValidationError
|
|
9
|
+
from tomlkit.exceptions import TOMLKitError
|
|
9
10
|
from tomlkit.toml_file import TOMLFile
|
|
11
|
+
from yaml import YAMLError
|
|
10
12
|
|
|
13
|
+
from rdetoolkit.exceptions import ConfigError
|
|
11
14
|
from rdetoolkit.models.config import Config, TracebackSettings, MultiDataTileSettings, SystemSettings, SmartTableSettings
|
|
12
15
|
from rdetoolkit.models.rde2types import RdeFsPath
|
|
13
16
|
|
|
@@ -16,6 +19,69 @@ PYPROJECT_CONFIG_FILES: Final = ["pyproject.toml"]
|
|
|
16
19
|
CONFIG_FILES = CONFIG_FILE + PYPROJECT_CONFIG_FILES
|
|
17
20
|
|
|
18
21
|
|
|
22
|
+
def _format_validation_error(
|
|
23
|
+
validation_error: ValidationError,
|
|
24
|
+
file_path: str,
|
|
25
|
+
) -> ConfigError:
|
|
26
|
+
"""Format pydantic ValidationError into user-friendly ConfigError.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
validation_error: The pydantic ValidationError
|
|
30
|
+
file_path: Path to the configuration file
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
ConfigError with detailed field-level information
|
|
34
|
+
"""
|
|
35
|
+
errors = validation_error.errors()
|
|
36
|
+
|
|
37
|
+
if not errors:
|
|
38
|
+
return ConfigError(
|
|
39
|
+
"Configuration validation failed",
|
|
40
|
+
file_path=file_path,
|
|
41
|
+
error_type="validation_error",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Take the first error for the main message
|
|
45
|
+
first_error = errors[0]
|
|
46
|
+
field_path = ".".join(str(loc) for loc in first_error["loc"])
|
|
47
|
+
error_msg = first_error["msg"]
|
|
48
|
+
error_type_detail = first_error["type"]
|
|
49
|
+
|
|
50
|
+
# Build detailed message
|
|
51
|
+
message_parts = [f"Invalid configuration in '{file_path}'"]
|
|
52
|
+
|
|
53
|
+
if field_path:
|
|
54
|
+
message_parts.append(f"Field '{field_path}' validation failed: {error_msg}")
|
|
55
|
+
else:
|
|
56
|
+
message_parts.append(f"Validation failed: {error_msg}")
|
|
57
|
+
|
|
58
|
+
# Add information about expected values if available
|
|
59
|
+
if "input" in first_error:
|
|
60
|
+
input_value = first_error["input"]
|
|
61
|
+
message_parts.append(f"Provided value: {input_value!r}")
|
|
62
|
+
|
|
63
|
+
# For extended_mode, provide specific guidance
|
|
64
|
+
if "extended_mode" in field_path and "enum" in error_type_detail.lower():
|
|
65
|
+
message_parts.append(
|
|
66
|
+
"Valid values for 'extended_mode': ['rdeformat', 'MultiDataTile']",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Add validation error context if multiple errors exist
|
|
70
|
+
if len(errors) > 1:
|
|
71
|
+
message_parts.append(
|
|
72
|
+
f"Note: {len(errors)} validation error(s) found. Showing the first one.",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
full_message = "\n".join(message_parts)
|
|
76
|
+
|
|
77
|
+
return ConfigError(
|
|
78
|
+
full_message,
|
|
79
|
+
file_path=file_path,
|
|
80
|
+
error_type="validation_error",
|
|
81
|
+
field_name=field_path,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
19
85
|
def parse_config_file(*, path: str | None = None) -> Config:
|
|
20
86
|
"""Parse the configuration file and return a Config object.
|
|
21
87
|
|
|
@@ -26,7 +92,7 @@ def parse_config_file(*, path: str | None = None) -> Config:
|
|
|
26
92
|
Config: The parsed configuration object.
|
|
27
93
|
|
|
28
94
|
Raises:
|
|
29
|
-
|
|
95
|
+
ConfigError: If the specified configuration file does not exist or cannot be parsed.
|
|
30
96
|
|
|
31
97
|
File Loading Priority:
|
|
32
98
|
1. If `path` is provided and the file extension is ".toml", the function will attempt to read the file as a TOML file.
|
|
@@ -39,10 +105,14 @@ def parse_config_file(*, path: str | None = None) -> Config:
|
|
|
39
105
|
- "pyproject.toml"
|
|
40
106
|
|
|
41
107
|
Note:
|
|
42
|
-
- If the specified
|
|
108
|
+
- If the specified file is not a recognized config file name (not in CONFIG_FILES),
|
|
109
|
+
an empty Config object will be returned.
|
|
110
|
+
- If the specified file does not exist, ConfigError is raised.
|
|
111
|
+
- If the file contains invalid YAML/TOML syntax, ConfigError is raised with line/column info.
|
|
112
|
+
- If the configuration fails validation, ConfigError is raised with field details.
|
|
43
113
|
|
|
44
114
|
Example:
|
|
45
|
-
parse_config_file(path="
|
|
115
|
+
parse_config_file(path="rdeconfig.yaml")
|
|
46
116
|
|
|
47
117
|
"""
|
|
48
118
|
config_data: dict[str, Any] = {
|
|
@@ -50,14 +120,57 @@ def parse_config_file(*, path: str | None = None) -> Config:
|
|
|
50
120
|
"multidata_tile": MultiDataTileSettings().model_dump(),
|
|
51
121
|
"smarttable": SmartTableSettings().model_dump(),
|
|
52
122
|
}
|
|
123
|
+
|
|
124
|
+
# Check file existence when path is provided
|
|
125
|
+
if path is not None:
|
|
126
|
+
path_obj = Path(path)
|
|
127
|
+
if not path_obj.exists():
|
|
128
|
+
msg = (
|
|
129
|
+
f"Configuration file not found: '{path}'. "
|
|
130
|
+
f"Create a configuration file or use 'rdetoolkit gen-config' to generate one."
|
|
131
|
+
)
|
|
132
|
+
raise ConfigError(
|
|
133
|
+
msg,
|
|
134
|
+
file_path=path,
|
|
135
|
+
error_type="file_not_found",
|
|
136
|
+
)
|
|
137
|
+
|
|
53
138
|
if path is not None and Path(path).name not in CONFIG_FILES:
|
|
54
139
|
return Config(system=SystemSettings(), multidata_tile=MultiDataTileSettings(), smarttable=SmartTableSettings())
|
|
55
140
|
|
|
56
141
|
if path is not None and is_toml(path):
|
|
57
142
|
config_data = __read_pyproject_toml(path)
|
|
58
143
|
elif path is not None and is_yaml(path):
|
|
59
|
-
|
|
60
|
-
|
|
144
|
+
try:
|
|
145
|
+
with open(path, encoding="utf-8") as f:
|
|
146
|
+
config_data = yaml.safe_load(f)
|
|
147
|
+
except YAMLError as e:
|
|
148
|
+
# Extract line and column information from YAMLError
|
|
149
|
+
line_number = None
|
|
150
|
+
column_number = None
|
|
151
|
+
|
|
152
|
+
if hasattr(e, "problem_mark") and e.problem_mark is not None:
|
|
153
|
+
line_number = e.problem_mark.line + 1 # YAML uses 0-indexed lines
|
|
154
|
+
column_number = e.problem_mark.column + 1
|
|
155
|
+
|
|
156
|
+
error_msg = "Failed to parse YAML file: invalid syntax"
|
|
157
|
+
if hasattr(e, "problem"):
|
|
158
|
+
error_msg = f"Failed to parse YAML file: {e.problem}"
|
|
159
|
+
|
|
160
|
+
raise ConfigError(
|
|
161
|
+
error_msg,
|
|
162
|
+
file_path=path,
|
|
163
|
+
error_type="parse_error",
|
|
164
|
+
line_number=line_number,
|
|
165
|
+
column_number=column_number,
|
|
166
|
+
) from e
|
|
167
|
+
except OSError as e:
|
|
168
|
+
msg = f"Failed to read YAML file: {e}"
|
|
169
|
+
raise ConfigError(
|
|
170
|
+
msg,
|
|
171
|
+
file_path=path,
|
|
172
|
+
error_type="io_error",
|
|
173
|
+
) from e
|
|
61
174
|
elif path is None:
|
|
62
175
|
project_path = Path.cwd()
|
|
63
176
|
pyproject_toml = project_path.joinpath(PYPROJECT_CONFIG_FILES[0])
|
|
@@ -68,19 +181,81 @@ def parse_config_file(*, path: str | None = None) -> Config:
|
|
|
68
181
|
if config_data is None:
|
|
69
182
|
return Config(system=SystemSettings(), multidata_tile=MultiDataTileSettings(), smarttable=SmartTableSettings())
|
|
70
183
|
|
|
71
|
-
|
|
184
|
+
try:
|
|
185
|
+
return Config(**config_data)
|
|
186
|
+
except ValidationError as e:
|
|
187
|
+
# Use helper to format validation error
|
|
188
|
+
if path:
|
|
189
|
+
raise _format_validation_error(e, path) from e
|
|
190
|
+
# Fallback for when path is None
|
|
191
|
+
msg = f"Configuration validation failed: {str(e)}"
|
|
192
|
+
raise ConfigError(
|
|
193
|
+
msg,
|
|
194
|
+
error_type="validation_error",
|
|
195
|
+
) from e
|
|
196
|
+
except TypeError as e:
|
|
197
|
+
# This occurs when config_data is not a mapping (e.g., list or string),
|
|
198
|
+
# which makes Config(**config_data) invalid. Normalize it to ConfigError.
|
|
199
|
+
base_msg = "Configuration data must be a mapping (key-value structure)"
|
|
200
|
+
msg = f"{base_msg} in file '{path}'" if path else base_msg
|
|
201
|
+
raise ConfigError(
|
|
202
|
+
msg,
|
|
203
|
+
file_path=path,
|
|
204
|
+
error_type="validation_error",
|
|
205
|
+
) from e
|
|
72
206
|
|
|
73
207
|
|
|
74
208
|
def __read_pyproject_toml(path: str) -> dict[str, Any]:
|
|
75
209
|
"""Read the pyproject.toml file and return the contents as a dictionary.
|
|
76
210
|
|
|
211
|
+
Args:
|
|
212
|
+
path: Path to the pyproject.toml file
|
|
213
|
+
|
|
77
214
|
Returns:
|
|
78
215
|
dict[str, Any]: The contents of the pyproject.toml file.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ConfigError: If the file does not exist or cannot be parsed
|
|
79
219
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
220
|
+
# Check file existence first
|
|
221
|
+
path_obj = Path(path)
|
|
222
|
+
if not path_obj.exists():
|
|
223
|
+
msg = (
|
|
224
|
+
f"Configuration file not found: '{path}'. "
|
|
225
|
+
f"Create a pyproject.toml file with [tool.rdetoolkit] section."
|
|
226
|
+
)
|
|
227
|
+
raise ConfigError(
|
|
228
|
+
msg,
|
|
229
|
+
file_path=path,
|
|
230
|
+
error_type="file_not_found",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
toml = TOMLFile(path)
|
|
235
|
+
obj = toml.read()
|
|
236
|
+
_obj = obj.unwrap()
|
|
237
|
+
return _obj.get("tool", {}).get("rdetoolkit", {})
|
|
238
|
+
except TOMLKitError as e:
|
|
239
|
+
# Extract line information if available
|
|
240
|
+
line_number = None
|
|
241
|
+
if hasattr(e, "line"):
|
|
242
|
+
line_number = e.line
|
|
243
|
+
|
|
244
|
+
error_msg = f"Failed to parse TOML file: {str(e)}"
|
|
245
|
+
|
|
246
|
+
raise ConfigError(
|
|
247
|
+
error_msg,
|
|
248
|
+
file_path=path,
|
|
249
|
+
error_type="parse_error",
|
|
250
|
+
line_number=line_number,
|
|
251
|
+
) from e
|
|
252
|
+
except OSError as e:
|
|
253
|
+
msg = f"Failed to read TOML file: {e}"
|
|
254
|
+
raise ConfigError(
|
|
255
|
+
msg,
|
|
256
|
+
file_path=path,
|
|
257
|
+
error_type="io_error",
|
|
258
|
+
) from e
|
|
84
259
|
|
|
85
260
|
|
|
86
261
|
def is_toml(filename: str) -> bool:
|
|
@@ -153,27 +328,32 @@ def get_config(target_dir_path: RdeFsPath) -> Config | None:
|
|
|
153
328
|
|
|
154
329
|
Returns:
|
|
155
330
|
Optional[Config]: The first valid configuration found, or None if no valid configuration is found.
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
ConfigError: If the target directory does not exist.
|
|
156
334
|
"""
|
|
157
335
|
if isinstance(target_dir_path, str):
|
|
158
336
|
target_dir_path = Path(target_dir_path)
|
|
159
337
|
if not target_dir_path.exists():
|
|
160
|
-
|
|
338
|
+
msg = (
|
|
339
|
+
f"Configuration directory not found: '{target_dir_path}'. "
|
|
340
|
+
f"Ensure the directory exists and contains a valid configuration file."
|
|
341
|
+
)
|
|
342
|
+
raise ConfigError(
|
|
343
|
+
msg,
|
|
344
|
+
file_path=str(target_dir_path),
|
|
345
|
+
error_type="directory_not_found",
|
|
346
|
+
)
|
|
161
347
|
for cfg_file in find_config_files(target_dir_path):
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
except ValidationError as e:
|
|
165
|
-
emsg = f"Invalid configuration file: {cfg_file}"
|
|
166
|
-
raise ValueError(emsg) from e
|
|
348
|
+
# parse_config_file now converts ValidationError to ConfigError internally
|
|
349
|
+
__config = parse_config_file(path=cfg_file)
|
|
167
350
|
if __config is not None:
|
|
168
351
|
return __config
|
|
169
352
|
|
|
170
353
|
pyproject_toml_path = get_pyproject_toml()
|
|
171
354
|
if pyproject_toml_path is not None:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
except ValidationError as e:
|
|
175
|
-
emsg = f"Invalid configuration file: {pyproject_toml_path}"
|
|
176
|
-
raise ValueError(emsg) from e
|
|
355
|
+
# parse_config_file now converts ValidationError to ConfigError internally
|
|
356
|
+
__config = parse_config_file(path=str(pyproject_toml_path))
|
|
177
357
|
if __config is not None:
|
|
178
358
|
return __config
|
|
179
359
|
return None
|
|
@@ -194,8 +374,16 @@ def load_config(tasksupport_path: RdeFsPath, *, config: Config | None = None) ->
|
|
|
194
374
|
if config is not None:
|
|
195
375
|
__config = config
|
|
196
376
|
else:
|
|
197
|
-
|
|
198
|
-
|
|
377
|
+
try:
|
|
378
|
+
__rtn_config = get_config(tasksupport_path)
|
|
379
|
+
__config = Config() if __rtn_config is None else __rtn_config
|
|
380
|
+
except ConfigError as e:
|
|
381
|
+
# Only swallow directory_not_found errors for backward compatibility
|
|
382
|
+
# Re-raise other ConfigError types (parse_error, validation_error, etc.)
|
|
383
|
+
if getattr(e, "error_type", None) == "directory_not_found":
|
|
384
|
+
__config = Config()
|
|
385
|
+
else:
|
|
386
|
+
raise
|
|
199
387
|
return __config
|
|
200
388
|
|
|
201
389
|
|
|
Binary file
|