workbook_resolver.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """
  2. Workbook resolution helpers for the Wuling overseas dealer expansion skill.
  3. Rules:
  4. - An explicit --excel path always wins.
  5. - Otherwise, prefer an existing project workbook in the current directory or parents.
  6. - Preview mode never creates a workbook.
  7. - Write mode may copy the blank skill template into the project directory.
  8. - Never write directly to the skill template.
  9. """
  10. from __future__ import annotations
  11. import shutil
  12. from pathlib import Path
  13. from typing import Any, Dict, Optional
  14. CANONICAL_WORKBOOK_NAME = "\u6469\u6d1b\u54e5\u5ba2\u6237\u5efa\u8054\u8868-\u6309\u6e20\u9053\u5206\u7c7b.xlsx"
  15. TEMPLATE_RELATIVE_PATH = Path("assets") / "blank_customer_outreach_workbook.xlsx"
  16. WORKBOOK_NAME_MARKERS = (
  17. "\u6469\u6d1b\u54e5\u5ba2\u6237\u5efa\u8054\u8868-\u6309\u6e20\u9053\u5206\u7c7b",
  18. "\u5ba2\u6237\u5efa\u8054\u8868",
  19. )
  20. EXCLUDE_NAME_MARKERS = (
  21. "~$",
  22. "_backup_",
  23. "backup_before",
  24. "_with_",
  25. "sent_",
  26. "preview",
  27. "candidate",
  28. "filtered",
  29. )
  30. def skill_root() -> Path:
  31. return Path(__file__).resolve().parents[2]
  32. def template_workbook_path(root: Optional[Path] = None) -> Path:
  33. return (root or skill_root()) / TEMPLATE_RELATIVE_PATH
  34. def _resolve_user_path(excel_path: str) -> Path:
  35. path = Path(excel_path).expanduser()
  36. if not path.is_absolute():
  37. path = Path.cwd() / path
  38. return path.resolve()
  39. def _excluded(path: Path) -> bool:
  40. name = path.name
  41. lowered = name.casefold()
  42. return name.startswith("~$") or any(marker.casefold() in lowered for marker in EXCLUDE_NAME_MARKERS)
  43. def _looks_like_workbook(path: Path) -> bool:
  44. if path.suffix.casefold() != ".xlsx" or _excluded(path):
  45. return False
  46. return any(marker in path.name for marker in WORKBOOK_NAME_MARKERS)
  47. def find_project_workbook(start_dir: Optional[Path] = None) -> Optional[Path]:
  48. """Find the newest likely outreach workbook in start_dir or its parents."""
  49. current = (start_dir or Path.cwd()).resolve()
  50. search_dirs = [current, *current.parents]
  51. for directory in search_dirs:
  52. if not directory.exists() or not directory.is_dir():
  53. continue
  54. candidates = [path for path in directory.glob("*.xlsx") if _looks_like_workbook(path)]
  55. if not candidates:
  56. continue
  57. exact = [path for path in candidates if path.name == CANONICAL_WORKBOOK_NAME]
  58. return sorted(exact or candidates, key=lambda path: path.stat().st_mtime, reverse=True)[0]
  59. return None
  60. def _copy_template_to(target: Path, root: Optional[Path] = None) -> Path:
  61. template = template_workbook_path(root)
  62. if not template.exists():
  63. raise FileNotFoundError(f"Blank workbook template not found: {template}")
  64. if template.resolve() == target.resolve():
  65. raise ValueError("Refusing to write directly to the skill blank workbook template.")
  66. target.parent.mkdir(parents=True, exist_ok=True)
  67. shutil.copy2(template, target)
  68. return target
  69. def resolve_workbook_path(
  70. excel_path: str = "",
  71. *,
  72. start_dir: Optional[Path] = None,
  73. create_from_template: bool = False,
  74. template_root: Optional[Path] = None,
  75. ) -> Dict[str, Any]:
  76. """
  77. Resolve the workbook path according to skill rules.
  78. Returns a dict with: path, source, created.
  79. source is one of explicit, explicit_template_copy, project, template_copy, missing.
  80. """
  81. if excel_path:
  82. path = _resolve_user_path(excel_path)
  83. if path.resolve() == template_workbook_path(template_root).resolve():
  84. raise ValueError("Do not write to the skill blank workbook template. Copy it to the project first.")
  85. if create_from_template and not path.exists():
  86. _copy_template_to(path, template_root)
  87. return {"path": path, "source": "explicit_template_copy", "created": True}
  88. return {"path": path, "source": "explicit", "created": False}
  89. project = find_project_workbook(start_dir)
  90. if project:
  91. return {"path": project, "source": "project", "created": False}
  92. if create_from_template:
  93. target_dir = (start_dir or Path.cwd()).resolve()
  94. target = target_dir / CANONICAL_WORKBOOK_NAME
  95. _copy_template_to(target, template_root)
  96. return {"path": target, "source": "template_copy", "created": True}
  97. return {"path": None, "source": "missing", "created": False}