@josephyan/qingflow-cli 0.2.0-beta.62 → 0.2.0-beta.64

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.
@@ -361,12 +361,32 @@ class ImportTools(ToolBase):
361
361
  )
362
362
  effective_path = path
363
363
  effective_local_check = local_check
364
- auto_normalization = self._maybe_auto_normalize_file(
365
- source_path=path,
366
- expected_columns=expected_columns,
367
- template_header_profile=template_header_profile,
368
- local_check=local_check,
369
- )
364
+ auto_normalization = None
365
+ try:
366
+ auto_normalization = self._maybe_auto_normalize_file(
367
+ source_path=path,
368
+ expected_columns=expected_columns,
369
+ template_header_profile=template_header_profile,
370
+ local_check=local_check,
371
+ )
372
+ except Exception as exc:
373
+ effective_local_check = deepcopy(local_check)
374
+ effective_local_check["issues"].append(
375
+ _issue(
376
+ "IMPORT_AUTO_NORMALIZATION_FAILED",
377
+ f"Workbook compatibility normalization failed before backend verification: {exc}",
378
+ severity="error",
379
+ )
380
+ )
381
+ effective_local_check["warnings"].append(
382
+ {
383
+ "code": "IMPORT_AUTO_NORMALIZATION_FAILED",
384
+ "message": "Workbook compatibility normalization failed during local precheck; returning a structured verification failure instead of crashing.",
385
+ }
386
+ )
387
+ effective_local_check["local_precheck_passed"] = False
388
+ effective_local_check["can_import"] = False
389
+ effective_local_check["error_code"] = "IMPORT_VERIFICATION_FAILED"
370
390
  if auto_normalization is not None:
371
391
  effective_path = Path(str(auto_normalization["verified_file_path"]))
372
392
  effective_local_check = self._local_verify(
@@ -456,7 +476,7 @@ class ImportTools(ToolBase):
456
476
  return {
457
477
  "ok": True,
458
478
  "status": "success" if can_import else "failed",
459
- "error_code": None if can_import else (local_check.get("error_code") or "IMPORT_VERIFICATION_FAILED"),
479
+ "error_code": None if can_import else (effective_local_check.get("error_code") or local_check.get("error_code") or "IMPORT_VERIFICATION_FAILED"),
460
480
  "can_import": can_import,
461
481
  "verification_id": verification_id,
462
482
  "file_path": str(path.resolve()),
@@ -475,14 +495,14 @@ class ImportTools(ToolBase):
475
495
  "import_auth_prechecked": precheck_known,
476
496
  "import_auth_precheck_passed": True if precheck_known else None,
477
497
  "import_auth_source": import_capability.get("auth_source"),
478
- "local_precheck_passed": bool(local_check["local_precheck_passed"]),
498
+ "local_precheck_passed": bool(effective_local_check["local_precheck_passed"]),
479
499
  "backend_verification_passed": isinstance(backend_verification, dict)
480
500
  and backend_verification.get("beingValidated", True) is not False,
481
501
  "schema_fingerprint": schema_fingerprint,
482
502
  "file_sha256": local_check["file_sha256"],
483
503
  "verified_file_sha256": effective_local_check["file_sha256"] if effective_path != path else None,
484
504
  "file_format": local_check["extension"],
485
- "local_precheck_limited": bool(local_check["local_precheck_limited"]),
505
+ "local_precheck_limited": bool(effective_local_check["local_precheck_limited"]),
486
506
  "auto_normalized": effective_path != path,
487
507
  },
488
508
  }
@@ -911,44 +931,59 @@ class ImportTools(ToolBase):
911
931
  base_result["can_import"] = False
912
932
  base_result["error_code"] = "IMPORT_VERIFICATION_FAILED"
913
933
  return base_result
914
- sheet = workbook[workbook.sheetnames[0]]
915
- header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
916
- header_analysis = _analyze_headers(
917
- header_row,
918
- expected_columns,
919
- allowed_titles=allowed_header_titles,
920
- )
921
- base_result["issues"].extend(header_analysis["issues"])
922
- base_result["repair_suggestions"].extend(header_analysis["repair_suggestions"])
923
- if not any(issue.get("severity") == "error" for issue in base_result["issues"]):
924
- semantic_issues, semantic_warnings = self._inspect_semantic_cells(
925
- profile=profile,
926
- context=context,
927
- sheet=sheet,
928
- expected_columns=expected_columns,
929
- field_index=field_index,
934
+ try:
935
+ sheet = workbook[workbook.sheetnames[0]]
936
+ header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
937
+ header_analysis = _analyze_headers(
938
+ header_row,
939
+ expected_columns,
940
+ allowed_titles=allowed_header_titles,
930
941
  )
931
- base_result["issues"].extend(semantic_issues)
932
- base_result["warnings"].extend(semantic_warnings)
933
- trailing_blank_rows = _count_trailing_blank_rows(sheet)
934
- if trailing_blank_rows > 0:
935
- base_result["warnings"].append(
936
- {
937
- "code": "TRAILING_BLANK_ROWS",
938
- "message": f"Workbook contains {trailing_blank_rows} trailing blank rows that can be safely removed.",
939
- }
942
+ base_result["issues"].extend(header_analysis["issues"])
943
+ base_result["repair_suggestions"].extend(header_analysis["repair_suggestions"])
944
+ if not any(issue.get("severity") == "error" for issue in base_result["issues"]):
945
+ semantic_issues, semantic_warnings = self._inspect_semantic_cells(
946
+ profile=profile,
947
+ context=context,
948
+ sheet=sheet,
949
+ expected_columns=expected_columns,
950
+ field_index=field_index,
951
+ )
952
+ base_result["issues"].extend(semantic_issues)
953
+ base_result["warnings"].extend(semantic_warnings)
954
+ trailing_blank_rows = _count_trailing_blank_rows(sheet)
955
+ if trailing_blank_rows > 0:
956
+ base_result["warnings"].append(
957
+ {
958
+ "code": "TRAILING_BLANK_ROWS",
959
+ "message": f"Workbook contains {trailing_blank_rows} trailing blank rows that can be safely removed.",
960
+ }
961
+ )
962
+ base_result["repair_suggestions"].append("trim_trailing_blank_rows")
963
+ enum_suggestions = _find_enum_repairs(sheet, expected_columns)
964
+ if enum_suggestions:
965
+ base_result["warnings"].append(
966
+ {
967
+ "code": "ENUM_VALUE_NORMALIZATION_AVAILABLE",
968
+ "message": "Some enum-like cells can be normalized to exact template values without changing meaning.",
969
+ }
970
+ )
971
+ base_result["repair_suggestions"].append("normalize_enum_values")
972
+ base_result["repair_suggestions"] = sorted(set(base_result["repair_suggestions"]))
973
+ except Exception as exc:
974
+ base_result["issues"].append(
975
+ _issue(
976
+ "IMPORT_LOCAL_PRECHECK_FAILED",
977
+ f"Workbook content could not be fully inspected during local precheck: {exc}",
978
+ severity="error",
979
+ )
940
980
  )
941
- base_result["repair_suggestions"].append("trim_trailing_blank_rows")
942
- enum_suggestions = _find_enum_repairs(sheet, expected_columns)
943
- if enum_suggestions:
944
981
  base_result["warnings"].append(
945
982
  {
946
- "code": "ENUM_VALUE_NORMALIZATION_AVAILABLE",
947
- "message": "Some enum-like cells can be normalized to exact template values without changing meaning.",
983
+ "code": "IMPORT_LOCAL_PRECHECK_FAILED",
984
+ "message": "Workbook local precheck encountered an unexpected compatibility problem; returning a structured verification failure instead of crashing.",
948
985
  }
949
986
  )
950
- base_result["repair_suggestions"].append("normalize_enum_values")
951
- base_result["repair_suggestions"] = sorted(set(base_result["repair_suggestions"]))
952
987
  if any(issue.get("severity") == "error" for issue in base_result["issues"]):
953
988
  base_result["local_precheck_passed"] = False
954
989
  base_result["can_import"] = False
@@ -1199,58 +1234,49 @@ class ImportTools(ToolBase):
1199
1234
  ) -> dict[str, Any] | None:
1200
1235
  if source_path.suffix.lower() != ".xlsx":
1201
1236
  return None
1202
- workbook = load_workbook(source_path, read_only=False, data_only=False)
1203
- if not workbook.sheetnames:
1204
- return None
1205
- sheet = workbook[workbook.sheetnames[0]]
1206
- header_depth = _infer_header_depth(sheet)
1207
- trailing_blank_rows = _count_trailing_blank_rows(sheet)
1208
- if header_depth <= 1 and trailing_blank_rows <= 0:
1209
- return None
1210
- extracted_headers = _extract_leaf_header_titles(sheet, header_depth)
1211
- target_headers = _overlay_header_titles(
1212
- extracted_headers,
1213
- template_header_profile.get("leaf_titles"),
1214
- )
1215
- verified_path = _resolve_verified_output_path(source_path)
1216
- normalized_workbook = Workbook()
1217
- normalized_sheet = normalized_workbook.active
1218
- normalized_sheet.title = sheet.title
1219
- normalized_sheet.append(target_headers)
1220
- last_nonblank_row = max(header_depth, sheet.max_row - trailing_blank_rows)
1221
- for row_index in range(header_depth + 1, last_nonblank_row + 1):
1222
- normalized_sheet.append(
1223
- [sheet.cell(row=row_index, column=column_index).value for column_index in range(1, sheet.max_column + 1)]
1237
+ try:
1238
+ workbook = load_workbook(source_path, read_only=False, data_only=False)
1239
+ if not workbook.sheetnames:
1240
+ return None
1241
+ sheet = workbook[workbook.sheetnames[0]]
1242
+ rows = [list(row) for row in sheet.iter_rows(values_only=True)]
1243
+ header_depth = _infer_header_depth(sheet)
1244
+ return _build_auto_normalized_file(
1245
+ source_path=source_path,
1246
+ sheet_title=sheet.title,
1247
+ rows=rows,
1248
+ header_depth=header_depth,
1249
+ template_leaf_titles=template_header_profile.get("leaf_titles"),
1250
+ local_check=local_check,
1224
1251
  )
1225
- verified_path.parent.mkdir(parents=True, exist_ok=True)
1226
- normalized_workbook.save(verified_path)
1227
- warnings: list[JSONObject] = []
1228
- applied_repairs: list[str] = []
1229
- if header_depth > 1:
1230
- applied_repairs.append("normalize_headers")
1231
- warnings.append(
1232
- {
1233
- "code": "IMPORT_HEADERS_AUTO_NORMALIZED",
1234
- "message": f"Workbook used {header_depth} header rows; record_import_verify normalized it to a single leaf-header row automatically.",
1235
- }
1252
+ except Exception as exc:
1253
+ workbook = load_workbook(source_path, read_only=True, data_only=False)
1254
+ if not workbook.sheetnames:
1255
+ return None
1256
+ sheet = workbook[workbook.sheetnames[0]]
1257
+ rows = [list(row) for row in sheet.iter_rows(values_only=True)]
1258
+ header_depth = _infer_header_depth_from_rows(
1259
+ rows,
1260
+ template_header_profile=template_header_profile,
1261
+ local_check=local_check,
1236
1262
  )
1237
- if trailing_blank_rows > 0:
1238
- applied_repairs.append("trim_trailing_blank_rows")
1239
- warnings.append(
1240
- {
1241
- "code": "TRAILING_BLANK_ROWS_AUTO_TRIMMED",
1242
- "message": f"Removed {trailing_blank_rows} trailing blank rows before backend verification.",
1243
- }
1263
+ normalized = _build_auto_normalized_file(
1264
+ source_path=source_path,
1265
+ sheet_title=sheet.title,
1266
+ rows=rows,
1267
+ header_depth=header_depth,
1268
+ template_leaf_titles=template_header_profile.get("leaf_titles"),
1269
+ local_check=local_check,
1244
1270
  )
1245
- return {
1246
- "verified_file_path": str(verified_path.resolve()),
1247
- "header_titles": target_headers,
1248
- "warnings": warnings,
1249
- "applied_repairs": applied_repairs,
1250
- "header_depth": header_depth,
1251
- "trailing_blank_rows": trailing_blank_rows,
1252
- "source_local_check": local_check,
1253
- }
1271
+ if normalized is not None:
1272
+ normalized["warnings"].insert(
1273
+ 0,
1274
+ {
1275
+ "code": "IMPORT_AUTO_NORMALIZATION_COMPATIBILITY_FALLBACK",
1276
+ "message": f"Workbook compatibility normalization retried in compatibility mode after a workbook parsing error: {exc}",
1277
+ },
1278
+ )
1279
+ return normalized
1254
1280
 
1255
1281
  def _fetch_import_capability(self, context, app_key: str) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
1256
1282
  try:
@@ -1748,6 +1774,107 @@ def _overlay_header_titles(actual_titles: list[str], template_leaf_titles: Any)
1748
1774
  return normalized
1749
1775
 
1750
1776
 
1777
+ def _infer_header_depth_from_rows(
1778
+ rows: list[list[Any]],
1779
+ *,
1780
+ template_header_profile: dict[str, Any],
1781
+ local_check: dict[str, Any],
1782
+ ) -> int:
1783
+ template_depth = max(1, int(template_header_profile.get("header_depth") or 1))
1784
+ header_depth = min(template_depth, max(1, len(rows)))
1785
+ if header_depth > 1:
1786
+ return header_depth
1787
+ if "normalize_headers" in (local_check.get("repair_suggestions") or []) and len(rows) >= 2:
1788
+ if any(_normalize_optional_text(value) for value in rows[1]):
1789
+ return 2
1790
+ return 1
1791
+
1792
+
1793
+ def _extract_leaf_header_titles_from_rows(rows: list[list[Any]], header_depth: int) -> list[str]:
1794
+ titles: list[str] = []
1795
+ max_column = max((len(row) for row in rows[: max(1, header_depth)]), default=0)
1796
+ depth = max(1, min(header_depth, len(rows)))
1797
+ for column_index in range(max_column):
1798
+ selected = ""
1799
+ for row_index in range(depth - 1, -1, -1):
1800
+ value = rows[row_index][column_index] if column_index < len(rows[row_index]) else None
1801
+ text = _normalize_optional_text(value)
1802
+ if text:
1803
+ selected = text
1804
+ break
1805
+ titles.append(selected)
1806
+ return titles
1807
+
1808
+
1809
+ def _count_trailing_blank_rows_from_rows(rows: list[list[Any]], *, min_data_index: int = 1) -> int:
1810
+ count = 0
1811
+ for row in reversed(rows[min_data_index:]):
1812
+ if any(value not in (None, "") for value in row):
1813
+ break
1814
+ count += 1
1815
+ return count
1816
+
1817
+
1818
+ def _build_auto_normalized_file(
1819
+ *,
1820
+ source_path: Path,
1821
+ sheet_title: str,
1822
+ rows: list[list[Any]],
1823
+ header_depth: int,
1824
+ template_leaf_titles: Any,
1825
+ local_check: dict[str, Any],
1826
+ ) -> dict[str, Any] | None:
1827
+ if not rows:
1828
+ return None
1829
+ normalized_header_depth = max(1, min(header_depth, len(rows)))
1830
+ trailing_blank_rows = _count_trailing_blank_rows_from_rows(rows, min_data_index=normalized_header_depth)
1831
+ if normalized_header_depth <= 1 and trailing_blank_rows <= 0:
1832
+ return None
1833
+ extracted_headers = _extract_leaf_header_titles_from_rows(rows, normalized_header_depth)
1834
+ target_headers = _overlay_header_titles(extracted_headers, template_leaf_titles)
1835
+ row_width = max(len(target_headers), max((len(row) for row in rows), default=0))
1836
+ if row_width <= 0:
1837
+ return None
1838
+ padded_headers = list(target_headers) + [""] * max(0, row_width - len(target_headers))
1839
+ verified_path = _resolve_verified_output_path(source_path)
1840
+ normalized_workbook = Workbook()
1841
+ normalized_sheet = normalized_workbook.active
1842
+ normalized_sheet.title = sheet_title
1843
+ normalized_sheet.append(padded_headers)
1844
+ last_nonblank_row = max(normalized_header_depth, len(rows) - trailing_blank_rows)
1845
+ for row in rows[normalized_header_depth:last_nonblank_row]:
1846
+ normalized_sheet.append(list(row) + [None] * max(0, row_width - len(row)))
1847
+ verified_path.parent.mkdir(parents=True, exist_ok=True)
1848
+ normalized_workbook.save(verified_path)
1849
+ warnings: list[JSONObject] = []
1850
+ applied_repairs: list[str] = []
1851
+ if normalized_header_depth > 1:
1852
+ applied_repairs.append("normalize_headers")
1853
+ warnings.append(
1854
+ {
1855
+ "code": "IMPORT_HEADERS_AUTO_NORMALIZED",
1856
+ "message": f"Workbook used {normalized_header_depth} header rows; record_import_verify normalized it to a single leaf-header row automatically.",
1857
+ }
1858
+ )
1859
+ if trailing_blank_rows > 0:
1860
+ applied_repairs.append("trim_trailing_blank_rows")
1861
+ warnings.append(
1862
+ {
1863
+ "code": "TRAILING_BLANK_ROWS_AUTO_TRIMMED",
1864
+ "message": f"Removed {trailing_blank_rows} trailing blank rows before backend verification.",
1865
+ }
1866
+ )
1867
+ return {
1868
+ "verified_file_path": str(verified_path.resolve()),
1869
+ "header_titles": target_headers or padded_headers,
1870
+ "warnings": warnings,
1871
+ "applied_repairs": applied_repairs,
1872
+ "header_depth": normalized_header_depth,
1873
+ "trailing_blank_rows": trailing_blank_rows,
1874
+ "source_local_check": local_check,
1875
+ }
1876
+
1877
+
1751
1878
  def _count_trailing_blank_rows(sheet) -> int: # type: ignore[no-untyped-def]
1752
1879
  count = 0
1753
1880
  for row_index in range(sheet.max_row, 1, -1):