overture-schema-cli 0.1.1.dev0__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,716 @@
1
+ """Data display utilities for verbose error output."""
2
+
3
+ from typing import Any
4
+
5
+ from rich import box
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+
9
+ # Display configuration constants
10
+ DEFAULT_FIELD_VALUE_MAX_LENGTH = 50
11
+ DEFAULT_CONTEXT_SIZE = 1
12
+
13
+
14
+ def _format_nested_path(error_path: list[str | int]) -> str:
15
+ """Format error path as displayable nested key.
16
+
17
+ Converts paths like ["sources", 0, "confidence"] into "sources[0].confidence".
18
+
19
+ Args
20
+ ----
21
+ error_path: List of field names (str) and array indices (int)
22
+
23
+ Returns
24
+ -------
25
+ Formatted path string with dots and brackets
26
+ """
27
+ parts: list[str] = []
28
+ for element in error_path:
29
+ if isinstance(element, str):
30
+ # Add dot separator before field names (except first, and not right after opening)
31
+ if parts and not parts[-1].endswith("]"):
32
+ parts.append(".")
33
+ elif parts and parts[-1].endswith("]"):
34
+ # Add dot after array index before field name
35
+ parts.append(".")
36
+ parts.append(element)
37
+ elif isinstance(element, int):
38
+ # Add array index in brackets
39
+ parts.append(f"[{element}]")
40
+ return "".join(parts)
41
+
42
+
43
+ def _has_nested_context(error_path: list[str | int]) -> bool:
44
+ """Check if error path represents nested context.
45
+
46
+ Returns True if the path has multiple fields or contains array indices.
47
+
48
+ Args
49
+ ----
50
+ error_path: Path to error field
51
+
52
+ Returns
53
+ -------
54
+ True if path is nested (multiple fields or has array index)
55
+ """
56
+ string_elements = sum(1 for e in error_path if isinstance(e, str))
57
+ has_array_index = any(isinstance(e, int) for e in error_path)
58
+ return string_elements > 1 or has_array_index
59
+
60
+
61
+ def extract_feature_data(
62
+ data: dict[str, Any] | list[Any] | Any, # noqa: ANN401
63
+ item_index: int | None,
64
+ ) -> dict[str, Any]:
65
+ """Extract and flatten a feature from various input formats.
66
+
67
+ Handles single features, lists of features, and GeoJSON FeatureCollections.
68
+ Flattens GeoJSON format (properties nested under 'properties' key) to
69
+ flat format (properties at top level).
70
+
71
+ Args
72
+ ----
73
+ data: Input data (dict, list, or FeatureCollection)
74
+ item_index: Index of item in list/collection, or None for single feature
75
+
76
+ Returns
77
+ -------
78
+ Flattened feature dict, or empty dict if extraction fails
79
+ """
80
+ try:
81
+ # Handle list of features
82
+ if isinstance(data, list):
83
+ if item_index is None or item_index < 0 or item_index >= len(data):
84
+ return {}
85
+ feature = data[item_index]
86
+ # Handle FeatureCollection
87
+ elif isinstance(data, dict) and data.get("type") == "FeatureCollection":
88
+ features = data.get("features", [])
89
+ if item_index is None or item_index < 0 or item_index >= len(features):
90
+ return {}
91
+ feature = features[item_index]
92
+ # Handle single feature
93
+ elif isinstance(data, dict):
94
+ feature = data
95
+ else:
96
+ return {}
97
+
98
+ # Flatten properties if in GeoJSON format
99
+ if isinstance(feature, dict) and "properties" in feature:
100
+ # Start with top-level fields (id, geometry, etc.)
101
+ flattened = {}
102
+ if "id" in feature:
103
+ flattened["id"] = feature["id"]
104
+ if "geometry" in feature:
105
+ flattened["geometry"] = feature["geometry"]
106
+
107
+ # Add properties at top level
108
+ properties = feature.get("properties", {})
109
+ if isinstance(properties, dict):
110
+ flattened.update(properties)
111
+
112
+ return flattened
113
+ elif isinstance(feature, dict):
114
+ # Already flat format
115
+ return feature
116
+ else:
117
+ return {}
118
+
119
+ except (TypeError, KeyError, AttributeError):
120
+ # Handle malformed or unexpected data structures gracefully
121
+ return {}
122
+
123
+
124
+ def _select_context_for_array_index_error(
125
+ feature: dict[str, Any],
126
+ error_path: list[str | int],
127
+ context_size: int,
128
+ ) -> dict[str, Any]:
129
+ """Handle error paths where the target is an array index.
130
+
131
+ When the error path ends with an array index (e.g., ["data_download_url", 0]),
132
+ we can't select "sibling fields" because array items don't have siblings.
133
+ Instead, show the parent array field with context.
134
+
135
+ Args
136
+ ----
137
+ feature: Flattened feature dict
138
+ error_path: Path ending with an array index
139
+ context_size: Number of neighboring fields to include
140
+
141
+ Returns
142
+ -------
143
+ Dict of selected fields showing context around the array
144
+ """
145
+ # Find the path up to the last string field (the array field itself)
146
+ array_field_path: list[str | int] = []
147
+ for element in error_path:
148
+ array_field_path.append(element)
149
+ if isinstance(element, str):
150
+ # Keep going until we hit an int, but remember the last string position
151
+ pass
152
+
153
+ # Find the last string element's index
154
+ last_string_idx = -1
155
+ for i, element in enumerate(error_path):
156
+ if isinstance(element, str):
157
+ last_string_idx = i
158
+
159
+ if last_string_idx < 0:
160
+ # No string elements at all - return empty
161
+ return {}
162
+
163
+ # Truncate path to end at the array field (last string element)
164
+ array_path = list(error_path[: last_string_idx + 1])
165
+
166
+ # Navigate to get the array value
167
+ current: Any = feature
168
+ for element in array_path[:-1]: # Navigate to parent of array field
169
+ if isinstance(current, dict) and isinstance(element, str):
170
+ current = current.get(element, {})
171
+ elif isinstance(current, list) and isinstance(element, int):
172
+ if 0 <= element < len(current):
173
+ current = current[element]
174
+ else:
175
+ return {}
176
+
177
+ # Now current should be the dict containing the array field
178
+ array_field_name = array_path[-1]
179
+ if not isinstance(array_field_name, str):
180
+ return {}
181
+
182
+ # Build the display with nested path notation
183
+ selected: dict[str, Any] = {}
184
+
185
+ # Format the full path including the array index
186
+ full_path_str = _format_nested_path(error_path)
187
+
188
+ if isinstance(current, dict):
189
+ # Get array value and the specific item
190
+ array_value = current.get(array_field_name)
191
+
192
+ # Find the array index in the error path
193
+ array_indices = [
194
+ e for e in error_path[last_string_idx + 1 :] if isinstance(e, int)
195
+ ]
196
+ if array_indices and isinstance(array_value, list):
197
+ idx = array_indices[0]
198
+ if 0 <= idx < len(array_value):
199
+ # Show the specific array item
200
+ selected[full_path_str] = array_value[idx]
201
+ else:
202
+ selected[full_path_str] = None
203
+ else:
204
+ # Show the whole array
205
+ selected[full_path_str] = array_value
206
+
207
+ # Add context fields from the parent dict
208
+ if len(array_path) > 1:
209
+ # Navigate to parent to get sibling fields
210
+ parent_path = array_path[:-1]
211
+ prefix = _format_nested_path(parent_path)
212
+ parent_fields = list(current.keys())
213
+
214
+ if array_field_name in parent_fields:
215
+ target_idx = parent_fields.index(array_field_name)
216
+ start = max(0, target_idx - context_size)
217
+ end = min(len(parent_fields), target_idx + context_size + 1)
218
+
219
+ for i in range(start, end):
220
+ field = parent_fields[i]
221
+ if field != array_field_name:
222
+ key = f"{prefix}.{field}" if prefix else field
223
+ selected[key] = current.get(field)
224
+ else:
225
+ # Fallback: just show the path
226
+ selected[full_path_str] = None
227
+
228
+ return selected
229
+
230
+
231
+ def select_context_fields(
232
+ feature: dict[str, Any],
233
+ error_path: list[str | int],
234
+ context_size: int = DEFAULT_CONTEXT_SIZE,
235
+ pinned_fields: list[str] | None = None,
236
+ ) -> dict[str, Any]:
237
+ """Select relevant fields for display around an error location.
238
+
239
+ Selects the error field plus neighboring fields for context. For nested
240
+ error paths (e.g., ["geometry", "type"] or ["sources", 0, "confidence"]),
241
+ navigates into the structure and selects the specific nested value.
242
+
243
+ Args
244
+ ----
245
+ feature: Flattened feature dict
246
+ error_path: Path to error field (may include array indices)
247
+ context_size: Number of neighboring fields to include on each side
248
+ pinned_fields: List of field names to always include (even outside context window)
249
+
250
+ Returns
251
+ -------
252
+ Dict of selected fields with their values
253
+ """
254
+ if not error_path:
255
+ return {}
256
+
257
+ # Navigate to the value at error_path
258
+ # Special case: don't navigate into geometry (it's treated as opaque)
259
+ current: Any = feature # Type can change as we navigate through nested structures
260
+ navigated_path = []
261
+ successfully_navigated = True
262
+
263
+ for i, element in enumerate(error_path):
264
+ navigated_path.append(element)
265
+
266
+ # Stop navigation if we encounter 'geometry' as a field name
267
+ if isinstance(element, str) and element == "geometry":
268
+ # Include geometry but don't navigate deeper
269
+ if isinstance(current, dict) and element in current:
270
+ current = current[element]
271
+ # Check if there are more elements after geometry
272
+ if i + 1 < len(error_path):
273
+ successfully_navigated = False
274
+ break
275
+
276
+ if isinstance(current, dict) and isinstance(element, str):
277
+ if element in current:
278
+ current = current[element]
279
+ else:
280
+ # Field doesn't exist - mark as None and stop navigating
281
+ current = None
282
+ successfully_navigated = False
283
+ break
284
+ elif isinstance(current, list) and isinstance(element, int):
285
+ if 0 <= element < len(current):
286
+ current = current[element]
287
+ else:
288
+ # Index out of range
289
+ current = None
290
+ successfully_navigated = False
291
+ break
292
+ else:
293
+ # Can't navigate further (e.g., trying to access a field on a non-dict)
294
+ # Keep current at the last successful level
295
+ successfully_navigated = False
296
+ break
297
+
298
+ # Determine if we're dealing with a nested path (has array index or multiple fields)
299
+ has_nested = _has_nested_context(error_path)
300
+
301
+ # If path contains an array index, include the entire array at top level
302
+ has_array_index = any(isinstance(e, int) for e in error_path)
303
+
304
+ # Initialize selected fields dict (populated in different branches below)
305
+ selected: dict[str, Any] = {}
306
+
307
+ # Check if navigation failed only on the last element (missing field case)
308
+ # In this case, we should still generate nested paths
309
+ navigation_failed_on_last = (
310
+ not successfully_navigated
311
+ and len(navigated_path) == len(error_path)
312
+ and current is None
313
+ )
314
+
315
+ if has_nested and (successfully_navigated or navigation_failed_on_last):
316
+ # For nested paths like ["sources", 0, "confidence"], show:
317
+ # 1. Top-level context around the parent field (e.g., fields around "sources")
318
+ # 2. Nested context within the parent object (e.g., fields around "confidence" in sources[0])
319
+
320
+ parent_path = error_path[:-1] # All but the last element
321
+ target_field = error_path[-1] # Last element (e.g., "confidence")
322
+
323
+ if not isinstance(target_field, str):
324
+ # Last element is an array index - show context around the parent array
325
+ # Navigate to find the deepest string field to use as context
326
+ return _select_context_for_array_index_error(
327
+ feature, error_path, context_size
328
+ )
329
+
330
+ # Get top-level field name (first string in path)
331
+ top_level_field = None
332
+ for element in error_path:
333
+ if isinstance(element, str):
334
+ top_level_field = element
335
+ break
336
+
337
+ if not top_level_field:
338
+ return {}
339
+
340
+ # First, add top-level context (fields around the parent field)
341
+ field_names = list(feature.keys())
342
+ if top_level_field in field_names:
343
+ target_index = field_names.index(top_level_field)
344
+ else:
345
+ field_names.append(top_level_field)
346
+ target_index = len(field_names) - 1
347
+
348
+ start_index = max(0, target_index - context_size)
349
+ end_index = min(len(field_names), target_index + context_size + 1)
350
+
351
+ # Add top-level elision marker if needed
352
+ if start_index > 0 and context_size > 0:
353
+ selected["..."] = "..."
354
+
355
+ # Add top-level context fields (but not the nested field itself, we'll add that with nested keys)
356
+ for i in range(start_index, end_index):
357
+ field_name = field_names[i]
358
+ if field_name != top_level_field:
359
+ selected[field_name] = feature.get(field_name)
360
+
361
+ # Add top-level elision marker at end if needed
362
+ if end_index < len(field_names) and context_size > 0:
363
+ selected["... "] = "..."
364
+
365
+ # Now navigate to parent and add nested context
366
+ parent: Any = feature # Type changes as we navigate through nested structures
367
+ for element in parent_path:
368
+ if isinstance(parent, dict) and isinstance(element, str):
369
+ parent = parent.get(element)
370
+ elif isinstance(parent, list) and isinstance(element, int):
371
+ if 0 <= element < len(parent):
372
+ parent = parent[element]
373
+ else:
374
+ return selected
375
+ else:
376
+ return selected
377
+
378
+ # Apply context selection within the parent object
379
+ if not isinstance(parent, dict):
380
+ return selected
381
+
382
+ parent_fields = list(parent.keys())
383
+ if target_field in parent_fields:
384
+ nested_target_index = parent_fields.index(target_field)
385
+ else:
386
+ parent_fields.append(target_field)
387
+ nested_target_index = len(parent_fields) - 1
388
+
389
+ nested_start = max(0, nested_target_index - context_size)
390
+ nested_end = min(len(parent_fields), nested_target_index + context_size + 1)
391
+
392
+ # Build path prefix once for reuse (without trailing dot)
393
+ prefix_str = _format_nested_path(parent_path)
394
+
395
+ # Add nested elision marker at start if needed
396
+ if nested_start > 0 and context_size > 0:
397
+ selected[f"{prefix_str}...."] = (
398
+ "..." # Dot + "..." for nested elision: sources[0]....
399
+ )
400
+
401
+ # Add nested fields with full paths
402
+ for i in range(nested_start, nested_end):
403
+ field_name = parent_fields[i]
404
+ full_key = f"{prefix_str}.{field_name}"
405
+ selected[full_key] = parent.get(field_name) # parent is verified dict above
406
+
407
+ # Add nested elision marker at end if needed
408
+ if nested_end < len(parent_fields) and context_size > 0:
409
+ selected[f"{prefix_str}.... "] = (
410
+ "..." # Space differentiates from start marker
411
+ )
412
+
413
+ return selected
414
+
415
+ # Get the top-level field name (first string element)
416
+ top_level_field = None
417
+ for element in error_path:
418
+ if isinstance(element, str):
419
+ top_level_field = element
420
+ break
421
+
422
+ if top_level_field is None:
423
+ return {}
424
+
425
+ # Get all field names in order
426
+ field_names = list(feature.keys())
427
+
428
+ # Find the target field
429
+ if top_level_field in field_names:
430
+ target_index = field_names.index(top_level_field)
431
+ else:
432
+ # Field is missing - still include it with None
433
+ field_names.append(top_level_field)
434
+ target_index = len(field_names) - 1
435
+
436
+ # Select fields within context window
437
+ start_index = max(0, target_index - context_size)
438
+ end_index = min(len(field_names), target_index + context_size + 1)
439
+
440
+ # Add "..." marker if fields were elided at the start (only if context_size > 0)
441
+ if start_index > 0 and context_size > 0:
442
+ selected["..."] = "..."
443
+
444
+ for i in range(start_index, end_index):
445
+ field_name = field_names[i]
446
+ if field_name in feature:
447
+ # For the error field with nested path, check if we should show the whole field or navigate
448
+ if field_name == top_level_field and len(error_path) > 1:
449
+ # If path has array index, include entire array/field
450
+ if has_array_index:
451
+ selected[field_name] = feature[field_name]
452
+ # Special case: if field is 'geometry', just show it without nested path
453
+ # since geometry is opaque
454
+ elif field_name == "geometry":
455
+ selected[field_name] = current
456
+ else:
457
+ # Build nested key with array indices: ["sources", 0, "confidence"] -> "sources[0].confidence"
458
+ nested_key = _format_nested_path(error_path)
459
+ selected[nested_key] = current
460
+ else:
461
+ selected[field_name] = feature[field_name]
462
+ else:
463
+ # Missing field - include with None
464
+ selected[field_name] = None
465
+
466
+ # Add "..." marker if fields were elided at the end (only if context_size > 0)
467
+ if end_index < len(field_names) and context_size > 0:
468
+ selected["... "] = (
469
+ "..." # Use "... " (with space) to distinguish from start marker
470
+ )
471
+
472
+ # Add pinned fields that aren't already in selected
473
+ # We need to maintain field order, so rebuild the dict
474
+ if pinned_fields:
475
+ # Build ordered dict with all fields (selected + pinned) in natural order
476
+ ordered_selected: dict[str, Any] = {}
477
+
478
+ # Add start elision marker
479
+ if "..." in selected:
480
+ ordered_selected["..."] = selected["..."]
481
+
482
+ # Add all fields in feature order (including pinned fields)
483
+ for field_name in field_names:
484
+ # Include if already selected OR if it's a pinned field
485
+ if field_name in selected:
486
+ ordered_selected[field_name] = selected[field_name]
487
+ elif field_name in pinned_fields:
488
+ ordered_selected[field_name] = feature.get(field_name)
489
+
490
+ # Add pinned fields that don't exist in the feature (at end with None)
491
+ for pinned_field in pinned_fields:
492
+ if pinned_field not in field_names and pinned_field not in ordered_selected:
493
+ ordered_selected[pinned_field] = None
494
+
495
+ # Add end elision marker
496
+ if "... " in selected:
497
+ ordered_selected["... "] = selected["... "]
498
+
499
+ return ordered_selected
500
+
501
+ return selected
502
+
503
+
504
+ def format_field_value(
505
+ value: object,
506
+ max_length: int = DEFAULT_FIELD_VALUE_MAX_LENGTH,
507
+ ) -> str:
508
+ """Format a field value for display.
509
+
510
+ Args
511
+ ----
512
+ value: Field value to format
513
+ max_length: Maximum length before truncation
514
+
515
+ Returns
516
+ -------
517
+ Formatted string representation
518
+ """
519
+ # Handle None (missing fields)
520
+ if value is None:
521
+ return "<missing>"
522
+
523
+ # Handle empty collections
524
+ if value == []:
525
+ return "[]"
526
+ if value == {}:
527
+ return "{}"
528
+
529
+ # Handle geometry objects
530
+ if isinstance(value, dict) and "type" in value:
531
+ geom_type = value.get("type")
532
+ if geom_type in {
533
+ "Point",
534
+ "LineString",
535
+ "Polygon",
536
+ "MultiPoint",
537
+ "MultiLineString",
538
+ "MultiPolygon",
539
+ "GeometryCollection",
540
+ }:
541
+ return str(geom_type)
542
+
543
+ # Handle nested objects - extract key info
544
+ if isinstance(value, dict):
545
+ # Try to extract primary field
546
+ if "primary" in value:
547
+ primary = value["primary"]
548
+ # Quote strings in nested display
549
+ if isinstance(primary, str):
550
+ return f'primary: "{primary}"'
551
+ return f"primary: {primary}"
552
+ # Otherwise show first few keys
553
+ keys = list(value.keys())[:3]
554
+ return "{" + ", ".join(f"{k}: ..." for k in keys) + "}"
555
+
556
+ # Handle arrays
557
+ if isinstance(value, list):
558
+ return f"[...{len(value)} items]"
559
+
560
+ # Handle strings - add quotes
561
+ if isinstance(value, str):
562
+ result = f'"{value}"'
563
+ # Truncate if too long (accounting for quotes)
564
+ if len(result) > max_length + 2:
565
+ return '"' + value[:max_length] + '..."'
566
+ return result
567
+
568
+ # Handle other primitives
569
+ result = str(value)
570
+
571
+ # Truncate if too long
572
+ if len(result) > max_length:
573
+ return result[:max_length] + "..."
574
+
575
+ return result
576
+
577
+
578
+ def create_feature_display(
579
+ fields: dict[str, Any],
580
+ errors: list[tuple[list[str | int], str]],
581
+ item_index: int | None = None,
582
+ item_type: str | None = None,
583
+ show_fields: list[str] | None = None,
584
+ feature: dict[str, Any] | None = None,
585
+ ) -> Panel:
586
+ """Create a Rich Panel with table displaying feature fields with error annotations.
587
+
588
+ Creates a borderless table with four columns: field names, values, arrows, and errors,
589
+ wrapped in a Panel with rounded borders. All error fields are annotated with
590
+ arrows and error messages, and highlighted in bright red.
591
+
592
+ Args
593
+ ----
594
+ fields: Dict of field names to values (in desired display order)
595
+ errors: List of (error_path, error_msg) tuples for all errors in this feature
596
+ item_index: Optional index of item in collection (for panel title)
597
+ item_type: Optional type name to display in panel title (e.g., "Building")
598
+ show_fields: List of field names to display in header
599
+ feature: Full feature dict for extracting show_fields values
600
+
601
+ Returns
602
+ -------
603
+ Rich Panel containing the table, ready to print
604
+ """
605
+ # Create table without borders (for alignment only)
606
+ table = Table(show_header=False, show_edge=False, box=None, padding=(0, 0, 0, 1))
607
+
608
+ # Add columns: Field (right-aligned) | Value (right-aligned) | Arrow | Error message
609
+ table.add_column("Field", no_wrap=True, justify="right")
610
+ table.add_column("Value", justify="right")
611
+ table.add_column("Arrow", no_wrap=True) # Single arrow column
612
+ table.add_column("Error")
613
+
614
+ # Build mapping from field display name to error messages
615
+ error_map: dict[str, list[str]] = {}
616
+ for error_path, error_msg in errors:
617
+ # Determine which field has the error (use first string in path)
618
+ error_field: str | None = None
619
+ for element in error_path:
620
+ if isinstance(element, str):
621
+ error_field = element
622
+ break
623
+
624
+ # Skip if no field name found (shouldn't happen in practice)
625
+ if error_field is None:
626
+ continue
627
+
628
+ # Format nested path for display
629
+ if len(error_path) > 1:
630
+ # Special case: geometry is opaque, don't show nested path
631
+ if error_field == "geometry":
632
+ error_field_display = "geometry"
633
+ else:
634
+ # Handle nested paths like ["sources", 0, "confidence"] -> "sources[0].confidence"
635
+ error_field_display = _format_nested_path(error_path)
636
+ else:
637
+ error_field_display = error_field
638
+
639
+ # Add to error map
640
+ if error_field_display not in error_map:
641
+ error_map[error_field_display] = []
642
+ error_map[error_field_display].append(error_msg)
643
+
644
+ # Add rows for each field
645
+ for field_name, field_value in fields.items():
646
+ # Check if this is an elision marker (handles both simple "..." and nested "sources[0]...")
647
+ is_elision = field_value == "..." and ("..." in field_name)
648
+
649
+ if is_elision:
650
+ # Display dimmed field name with blank value (4 columns now)
651
+ table.add_row(f"[dim]{field_name}[/dim]", "", "", "")
652
+ continue
653
+
654
+ formatted_value = format_field_value(field_value)
655
+
656
+ # Check if this field has errors
657
+ if field_name in error_map:
658
+ # Add error annotation with arrow in separate column
659
+ field_name_styled = f"[bold bright_yellow]{field_name}[/bold bright_yellow]"
660
+ value_styled = f"[bright_red]{formatted_value}[/bright_red]"
661
+ # Join multiple error messages with newlines (without arrows)
662
+ error_messages = error_map[field_name]
663
+ error_text = "\n".join(f"[blue]{msg}[/blue]" for msg in error_messages)
664
+ # Arrow goes in its own column
665
+ arrow = "[cyan]←[/cyan]"
666
+ table.add_row(field_name_styled, value_styled, arrow, error_text)
667
+ else:
668
+ # Apply cyan style to non-error fields, dim the value for context
669
+ field_name_styled = f"[cyan]{field_name}[/cyan]"
670
+ value_styled = f"[dim]{formatted_value}[/dim]"
671
+ table.add_row(field_name_styled, value_styled, "", "")
672
+
673
+ # Wrap table in a Panel with rounded borders
674
+ # Add title: "Validation Failed" for single features, or item info for collections
675
+ if item_index is not None:
676
+ if item_type:
677
+ title = f"[{item_index}] ({item_type})"
678
+ else:
679
+ title = f"[{item_index}]"
680
+ else:
681
+ title = "[bright_red]Validation Failed[/bright_red]"
682
+
683
+ # Add show_fields to title if provided
684
+ if show_fields and feature:
685
+ field_parts = []
686
+ max_field_length = 30 # Truncate long values in header
687
+ for field_name in show_fields:
688
+ field_value = feature.get(field_name)
689
+ if field_value is None:
690
+ formatted = "<missing>"
691
+ elif isinstance(field_value, str):
692
+ # Truncate long strings
693
+ if len(field_value) > max_field_length:
694
+ formatted = field_value[:max_field_length] + "..."
695
+ else:
696
+ formatted = field_value
697
+ else:
698
+ # For non-strings, convert to string and truncate
699
+ str_value = str(field_value)
700
+ if len(str_value) > max_field_length:
701
+ formatted = str_value[:max_field_length] + "..."
702
+ else:
703
+ formatted = str_value
704
+ field_parts.append(f"{field_name}={formatted}")
705
+
706
+ if field_parts:
707
+ title = f"{title} {' '.join(field_parts)}"
708
+
709
+ return Panel(
710
+ table,
711
+ box=box.HORIZONTALS,
712
+ border_style="bright_black",
713
+ expand=True,
714
+ title=title,
715
+ title_align="left",
716
+ )