pypi.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. # Copyright Materialize, Inc. and contributors. All rights reserved.
  2. #
  3. # Use of this software is governed by the Business Source License
  4. # included in the LICENSE file at the root of this repository.
  5. #
  6. # As of the Change Date specified in that file, in accordance with
  7. # the Business Source License, use of this software will be governed
  8. # by the Apache License, Version 2.0.
  9. import distutils.core # pyright: ignore
  10. import os
  11. import sys
  12. import tarfile
  13. import tempfile
  14. from email.parser import Parser
  15. from pathlib import Path
  16. from typing import Literal
  17. import requests
  18. from materialize import spawn
  19. PACKAGE_PATHS: dict[str, Literal["setup.py", "pyproject.toml"]] = {
  20. "misc/dbt-materialize": "setup.py",
  21. "misc/mcp-materialize": "pyproject.toml",
  22. }
  23. def main() -> None:
  24. for path_str, build_type in PACKAGE_PATHS.items():
  25. path = Path(path_str)
  26. if build_type == "setup.py":
  27. name, version = get_metadata_from_setup_py(path)
  28. build_package_setup_py(path)
  29. elif build_type == "pyproject.toml":
  30. name, version = get_metadata_from_pyproject(path)
  31. build_package_pyproject(path)
  32. else:
  33. raise ValueError(f"Unknown build type: {build_type}")
  34. assert name and version
  35. released_versions = get_released_versions(name)
  36. if version in released_versions:
  37. print(f"{name} {version} already released, continuing...")
  38. continue
  39. print(f"Releasing {name} {version}")
  40. upload_to_pypi(path)
  41. def get_metadata_from_setup_py(path: Path) -> tuple[str, str]:
  42. distribution = distutils.core.run_setup(str(path / "setup.py"))
  43. return distribution.metadata.name, distribution.metadata.version
  44. def get_metadata_from_pyproject(path: Path) -> tuple[str, str]:
  45. import build
  46. with tempfile.TemporaryDirectory() as out_dir:
  47. builder = build.ProjectBuilder(path)
  48. sdist_path = Path(builder.build("sdist", out_dir))
  49. with tarfile.open(sdist_path) as tar:
  50. pkg_info = next(m for m in tar.getmembers() if m.name.endswith("PKG-INFO"))
  51. f = tar.extractfile(pkg_info)
  52. if not f:
  53. raise RuntimeError("Failed to extract PKG-INFO")
  54. metadata = Parser().parsestr(f.read().decode())
  55. return metadata["Name"], metadata["Version"]
  56. def build_package_setup_py(path: Path) -> None:
  57. spawn.runv([sys.executable, "setup.py", "build", "sdist"], cwd=path)
  58. def build_package_pyproject(path: Path) -> None:
  59. spawn.runv([sys.executable, "-m", "build"], cwd=path)
  60. def upload_to_pypi(path: Path) -> None:
  61. dist_files = list((path / "dist").iterdir())
  62. spawn.runv(
  63. ["twine", "upload", *dist_files],
  64. env={
  65. **os.environ,
  66. "TWINE_USERNAME": "__token__",
  67. "TWINE_PASSWORD": os.environ["PYPI_TOKEN"],
  68. },
  69. )
  70. def get_released_versions(name: str) -> set[str]:
  71. # Default user client currently fails:
  72. # > python-requests/2.32.3 User-Agents are currently blocked from accessing
  73. # > JSON release resources. A cluster is apparently crawling all
  74. # > project/release resources resulting in excess cache misses. Please
  75. # > contact admin@pypi.org if you have information regarding what this
  76. # > software may be.
  77. res = requests.get(
  78. f"https://pypi.org/pypi/{name}/json",
  79. headers={"User-Agent": "Materialize Version Check"},
  80. )
  81. if res.status_code == 404:
  82. # First release, no versions exist yet
  83. return set()
  84. res.raise_for_status()
  85. return set(res.json()["releases"])
  86. if __name__ == "__main__":
  87. main()