Source code for stouputils.continuous_delivery.pypi

""" This module contains utilities for PyPI.
(Using build and twine packages)

- pypi_full_routine: Upload the most recent file(s) to PyPI after updating pip and required packages and building the package (using build and twine)
- pypi_full_routine_using_uv: Full build and publish routine using 'uv' command line tool

.. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/continuous_delivery/pypi_module.gif
  :alt: stouputils pypi examples
"""

# Imports
import os
import subprocess
import sys
from collections.abc import Callable
from typing import Any

from ..decorators import LogLevels, handle_error
from .pyproject import read_pyproject


[docs] def update_pip_and_required_packages() -> int: """ Update pip and required packages. Returns: int: Return code of the subprocess.run call. """ return subprocess.run(f"{sys.executable} -m pip install --upgrade pip setuptools build twine pkginfo packaging", shell=True).returncode
[docs] def build_package() -> int: """ Build the package. Returns: int: Return code of the subprocess.run call. """ return subprocess.run(f"{sys.executable} -m build", shell=True).returncode
[docs] def upload_package(repository: str, filepath: str) -> int: """ Upload the package to PyPI. Args: repository (str): Repository to upload to. filepath (str): Path to the file to upload. Returns: int: Return code of the subprocess.run call. """ return subprocess.run(f"{sys.executable} -m twine upload --verbose -r {repository} {filepath}", shell=True).returncode
[docs] @handle_error(message="Error while doing the pypi full routine", error_log=LogLevels.ERROR_TRACEBACK) def pypi_full_routine( repository: str, dist_directory: str, last_files: int = 1, endswith: str = ".tar.gz", update_all_function: Callable[[], int] = update_pip_and_required_packages, build_package_function: Callable[[], int] = build_package, upload_package_function: Callable[[str, str], int] = upload_package, ) -> None: """ Upload the most recent file(s) to PyPI after updating pip and required packages and building the package. Args: repository (str): Repository to upload to. dist_directory (str): Directory to upload from. last_files (int): Number of most recent files to upload. Defaults to 1. endswith (str): End of the file name to upload. Defaults to ".tar.gz". update_all_function (Callable[[], int]): Function to update pip and required packages. Defaults to :func:`update_pip_and_required_packages`. build_package_function (Callable[[], int]): Function to build the package. Defaults to :func:`build_package`. upload_package_function (Callable[[str, str], int]): Function to upload the package. Defaults to :func:`upload_package`. Returns: int: Return code of the command. """ if update_all_function() != 0: raise Exception("Error while updating pip and required packages") if build_package_function() != 0: raise Exception("Error while building the package") # Get list of tar.gz files in dist directory sorted by modification time files: list[str] = sorted( [x for x in os.listdir(dist_directory) if x.endswith(endswith)], # Get list of tar.gz files in dist directory key=lambda x: os.path.getmtime(f"{dist_directory}/{x}"), # Sort by modification time reverse=True # Sort in reverse order ) # Upload the most recent file(s) for file in files[:last_files]: upload_package_function(repository, f"{dist_directory}/{file}")
[docs] def pypi_full_routine_using_uv() -> None: """ Full build and publish routine using 'uv' command line tool. Steps: 1. Generate stubs unless '--no-stubs' is passed 2. Increment version in pyproject.toml (patch by default, minor if 'minor' is passed as last argument, 'major' if 'major' is passed) 3. Build the package using 'uv build' 4. Upload the most recent file to PyPI using 'uv publish' """ # Get package name from pyproject.toml pyproject_data: dict[str, Any] = read_pyproject("pyproject.toml") package_name: str = pyproject_data["project"]["name"] package_dir: str = package_name if not os.path.isdir(package_dir): package_dir = "src/" + package_name # Generate stubs unless '--no-stubs' is passed if "--no-stubs" not in sys.argv and "--no_stubs" not in sys.argv: from .stubs import stubs_full_routine stubs_full_routine(package_name, output_directory=os.path.dirname(package_dir) or ".", clean_before=True) # Increment version in pyproject.toml if "--no-bump" not in sys.argv and "--no_bump" not in sys.argv: increment: str = "patch" if sys.argv[-1] not in ("minor", "major") else sys.argv[-1] if subprocess.run(f"uv version --bump {increment} --frozen", shell=True).returncode != 0: raise Exception("Error while incrementing version using 'uv version'") # Build the package using 'uv build' import shutil shutil.rmtree("dist", ignore_errors=True) if subprocess.run(f"{sys.executable} -m uv build", shell=True).returncode != 0: raise Exception("Error while building the package using 'uv build'") # Upload the most recent file to PyPI using 'uv publish' if subprocess.run(f"{sys.executable} -m uv publish", shell=True).returncode != 0: raise Exception("Error while publishing the package using 'uv publish'")