hatch_build.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """
  2. Custom build hook for Agency Swarm.
  3. This hook downloads the latest pricing data from LiteLLM before building the package.
  4. The pricing file is:
  5. 1. Downloaded automatically on `main` before each build (via this hook)
  6. 2. Included in the package artifacts (via pyproject.toml)
  7. 3. Committed to the repo so tests can run without network access
  8. """
  9. import importlib
  10. import logging
  11. import subprocess
  12. import tempfile
  13. from pathlib import Path
  14. BuildHookInterface: type = object
  15. try:
  16. _iface = importlib.import_module("hatchling.builders.hooks.plugin.interface")
  17. BuildHookInterface = _iface.BuildHookInterface
  18. except Exception:
  19. # Hatchling is a build-time dependency; it may not be installed in dev/test environments.
  20. # The build hook will still work when invoked by hatchling (where it is installed).
  21. BuildHookInterface = object
  22. logger = logging.getLogger(__name__)
  23. # URL to the LiteLLM pricing file
  24. PRICING_FILE_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
  25. # Target path relative to the project root (resolved via self.root in the hook)
  26. PRICING_FILE_RELATIVE_PATH = Path("src/agency_swarm/data/model_prices_and_context_window.json")
  27. def _warn_if_pricing_file_missing(pricing_file_path: Path) -> None:
  28. if pricing_file_path.exists():
  29. return
  30. logger.error(f"Pricing file not found at {pricing_file_path}. Usage cost tracking may be unavailable at runtime.")
  31. def _get_git_branch(root: str) -> str | None:
  32. try:
  33. proc = subprocess.run(
  34. ["git", "rev-parse", "--abbrev-ref", "HEAD"],
  35. cwd=root,
  36. check=True,
  37. capture_output=True,
  38. text=True,
  39. )
  40. except Exception:
  41. return None
  42. branch = proc.stdout.strip()
  43. return branch or None
  44. class CustomBuildHook(BuildHookInterface):
  45. """Build hook that downloads the latest pricing data before building."""
  46. def initialize(self, version, build_data):
  47. """Download pricing data file before build."""
  48. try:
  49. import urllib.request
  50. # Resolve path relative to the build root (hatchling may run from a different CWD)
  51. pricing_file_path = Path(self.root) / PRICING_FILE_RELATIVE_PATH
  52. # Ensure the data directory exists
  53. pricing_file_path.parent.mkdir(parents=True, exist_ok=True)
  54. branch = _get_git_branch(str(self.root))
  55. if branch != "main":
  56. # When builds skip the download (non-main branches or when git info isn't available),
  57. # still validate the pricing file exists so cost tracking doesn't silently disappear.
  58. _warn_if_pricing_file_missing(pricing_file_path)
  59. logger.info(
  60. "Skipping pricing data download (branch is not 'main'). "
  61. "Set branch to 'main' to auto-refresh this file during builds."
  62. )
  63. return
  64. logger.info(f"Downloading pricing data from {PRICING_FILE_URL}...")
  65. with urllib.request.urlopen(PRICING_FILE_URL) as resp:
  66. downloaded = resp.read()
  67. if pricing_file_path.exists():
  68. existing = pricing_file_path.read_bytes()
  69. if existing == downloaded:
  70. logger.info("Pricing data is already up to date; no file changes needed.")
  71. return
  72. with tempfile.NamedTemporaryFile(dir=pricing_file_path.parent, delete=False) as tmp:
  73. tmp.write(downloaded)
  74. tmp_path = Path(tmp.name)
  75. tmp_path.replace(pricing_file_path)
  76. logger.info(f"Successfully updated pricing data at {pricing_file_path}")
  77. # The file is already included in pyproject.toml artifacts, so we don't need to add it here
  78. # but we ensure it exists before the build proceeds
  79. except Exception as e:
  80. logger.warning(f"Failed to download pricing data: {e}. Build will continue with existing file if present.")
  81. # Don't fail the build if download fails - use existing file or handle gracefully
  82. pricing_file_path = Path(self.root) / PRICING_FILE_RELATIVE_PATH
  83. _warn_if_pricing_file_missing(pricing_file_path)
  84. # Export the hook class for hatchling to discover
  85. __all__ = ["CustomBuildHook"]