cal-publish-python 0.7.0
CLI tool to publish Python packages and documentation with external configuration
Requires Python: >=3.12

# cal-publish-python

CLI tool to publish Python packages and documentation with external configuration.

## Overview

`cal-publish-python` is designed to keep sensitive configuration (tokens, registry URLs, SSH keys) outside of build jobs. The project being published defines the "what" (name, version), while a named profile in the publishing configuration defines the "where" (registry, documentation platform).

## Installation

```bash
pip install cal-publish-python
```

Or with uv:

```bash
uv pip install cal-publish-python
```

## Usage

```bash
# Publish everything in output/ (wheel + docs zip)
cal-publish-python --set-latest output/

# Publish only the PyPI package
cal-publish-python --pypi-only output/

# Publish only the documentation
cal-publish-python --docs-only --set-latest output/

# Specify a profile explicitly
cal-publish-python --profile internal --set-latest output/

# Specify individual files
cal-publish-python output/my-package-1.0.0-py3-none-any.whl output/my-package-1.0.0-docs.zip
```

### CLI Arguments

| Argument | Short | Description |
|----------|-------|-------------|
| `ARTIFACT` | | Directory or file(s) to publish (.whl and/or docs .zip) |
| `--config` | `-c` | Path to JSON configuration file |
| `--profile` | | Profile to use from the configuration file |
| `--pypi-only` | | Only publish to PyPI (skip documentation) |
| `--docs-only` | | Only publish documentation (skip PyPI) |
| `--set-latest` | `-l` | Also set this version as 'latest' (GitLab Pages mode only) |
| `--force` | `-f` | Overwrite existing documentation version if it exists |
| `--verbose` | `-v` | Verbose output |

## Configuration

### Profiles

Configuration uses named profiles. Each profile contains the full set of publishing settings (PyPI credentials, documentation targets, etc.). This allows the same config file to serve both public and internal publishing.

Create a JSON config file at `~/.config/cal-publish-python/config.json`:

```json
{
    "profiles": {
        "public": {
            "pypi": {
                "token": "pypi-xxxxxxxxxxxx",
                "username": "__token__",
                "repository_url": "https://upload.pypi.org/legacy/"
            },
            "docs": {
                "mode": "gitlab-pages",
                "gitlab_pages": {
                    "token": "glpat-xxxxxxxxxxxx",
                    "url": "https://gitlab.com",
                    "group": "mygroup/public/docs"
                }
            }
        },
        "internal": {
            "pypi": {
                "token": "nexus-xxxxxxxxxxxx",
                "username": "deploy-user",
                "repository_url": "https://nexus.internal/repository/pypi/"
            },
            "docs": {
                "mode": "ssh",
                "ssh": {
                    "host": "docs.internal",
                    "base_path": "/var/www/docs"
                }
            }
        }
    }
}
```

### Profile Selection

A profile is required when a config file is present. The profile is resolved in this order:

1. `--profile NAME` CLI argument
2. `CAL_PUBLISH_PROFILE` environment variable
3. `[tool.cal-publish-python]` in `pyproject.toml`:

```toml
[tool.cal-publish-python]
profile = "public"
```

This means each project can declare which profile it uses, and `make publish` just works without any extra arguments.

### Config File Location

The config file is resolved in this order:

1. `--config FILE` CLI argument
2. `CAL_PUBLISH_CONFIG` environment variable
3. `~/.config/cal-publish-python/config.json` (default)

If no config file exists at the default path, the tool runs in environment-variable-only mode (useful for CI).

### Environment Variables

All configuration values can be overridden by environment variables, regardless of what the config file/profile contains:

| Variable | Description |
|----------|-------------|
| `CAL_PUBLISH_CONFIG` | Path to JSON config file |
| `CAL_PUBLISH_PROFILE` | Profile name to use from config file |
| `CAL_PUBLISH_PYPI_TOKEN` | PyPI API token or password |
| `CAL_PUBLISH_PYPI_USERNAME` | PyPI username (default: `__token__`) |
| `CAL_PUBLISH_PYPI_REPOSITORY_URL` | PyPI repository URL |
| `CAL_PUBLISH_DOCS_MODE` | Documentation mode (`gitlab-pages`, `ssh`, or `cal-docs-server`) |
| `CAL_PUBLISH_GITLAB_TOKEN` | GitLab personal access token |
| `CAL_PUBLISH_GITLAB_URL` | GitLab instance URL |
| `CAL_PUBLISH_GITLAB_GROUP` | GitLab group for docs |
| `CAL_PUBLISH_SSH_HOST` | SSH host |
| `CAL_PUBLISH_SSH_BASE_PATH` | Base path on remote server |
| `CAL_PUBLISH_SSH_KEY` | Path to SSH private key |
| `CAL_PUBLISH_SSH_USER` | SSH user |
| `CAL_PUBLISH_SSH_PORT` | SSH port |
| `CAL_PUBLISH_DOCS_SERVER_URL` | cal-docs-server URL |
| `CAL_PUBLISH_DOCS_SERVER_TOKEN` | API token for cal-docs-server |

## Integration with Makefile

Add a `publish` target to your project's Makefile:

```makefile
.PHONY: publish
publish: check-dependencies
	uv run cal-publish-python --set-latest output/
```

And declare the profile in `pyproject.toml`:

```toml
[tool.cal-publish-python]
profile = "public"
```

### Documentation Zip Filename

The docs zip filename is used to determine the project name and version:

```
project-name-VERSION-docs.zip
```

Examples: `myproject-1.0.0-docs.zip`, `my-lib-2.3.4.post5+gabcdef-docs.zip`

## Development

```bash
# Clone the repository
git clone <repo-url>
cd cal-publish-python

# Set up development environment
make dev

# Run checks
make check

# Build
make build
```

## Licence

MIT License - Copyright (c) 2026 Cyber Assessment Labs