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' """ # Show help message if '--help', '-h', or 'help' is passed if any(arg in sys.argv for arg in ("--help", "-h", "help")): from ..config import StouputilsConfig as Cfg separator: str = "─" * 60 print(f""" {Cfg.CYAN}{separator}{Cfg.RESET} {Cfg.CYAN}stouputils {Cfg.GREEN}build{Cfg.RESET} — Build and publish package to PyPI using 'uv' {Cfg.CYAN}{separator}{Cfg.RESET} {Cfg.CYAN}Usage:{Cfg.RESET} stouputils build [options] [patch|minor|major] {Cfg.CYAN}Options:{Cfg.RESET} {Cfg.GREEN}--no_stubs {Cfg.RESET} Skip stub generation and deletion {Cfg.GREEN}--keep_stubs {Cfg.RESET} Keep stub files after upload (implies stub generation) {Cfg.GREEN}--no_bump {Cfg.RESET} Skip version increment {Cfg.GREEN}--no_publish {Cfg.RESET} Skip uploading to PyPI {Cfg.GREEN}patch | minor | major {Cfg.RESET} Version increment type (default: patch) {Cfg.GREEN}--help, -h, help {Cfg.RESET} Show this help message {Cfg.CYAN}{separator}{Cfg.RESET} """.strip()) return # 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 not any(arg in sys.argv for arg in ("--no-upload", "--no_upload", "--no-publish", "--no_publish")): if subprocess.run(f"{sys.executable} -m uv publish", shell=True).returncode != 0: raise Exception("Error while publishing the package using 'uv publish'") # Delete all stub files unless '--no-stubs', '--no_stubs', '--keep-stubs', or '--keep_stubs' is passed if not any(arg in sys.argv for arg in ("--no-stubs", "--no_stubs", "--keep-stubs", "--keep_stubs")): from .stubs import clean_stubs_directory clean_stubs_directory(os.path.dirname(package_dir) or ".", package_name)