Metadata-Version: 2.5
Name: wads
Version: 0.2.19
Summary: Tools for packaging and publishing to pypi for those who just do not want to deal with it
Project-URL: Homepage, https://github.com/i2mint/wads
Author: Thor Whalen
License: Apache Software License
License-File: LICENSE
Keywords: documentation,packaging,publishing
Requires-Python: >=3.10
Requires-Dist: argh
Requires-Dist: jinja2
Requires-Dist: packaging
Requires-Dist: pyyaml
Requires-Dist: tomli-w>=0.4.0
Requires-Dist: tomli>=1.0.0; python_version < '3.11'
Provides-Extra: all
Requires-Dist: build; extra == 'all'
Requires-Dist: epythet; extra == 'all'
Requires-Dist: requests; extra == 'all'
Requires-Dist: ruamel-yaml; extra == 'all'
Requires-Dist: tomlkit; extra == 'all'
Requires-Dist: wheel; extra == 'all'
Provides-Extra: create
Requires-Dist: build; extra == 'create'
Requires-Dist: requests; extra == 'create'
Requires-Dist: ruamel-yaml; extra == 'create'
Requires-Dist: tomlkit; extra == 'create'
Requires-Dist: wheel; extra == 'create'
Provides-Extra: docs
Requires-Dist: epythet; extra == 'docs'
Provides-Extra: skills
Requires-Dist: skill; extra == 'skills'
Provides-Extra: test
Requires-Dist: pytest-cov; extra == 'test'
Requires-Dist: pytest>=7.0; extra == 'test'
Description-Content-Type: text/markdown

# wads

Modern Python project packaging and CI/CD tools for developers who want to focus on code, not configuration.

[![PyPI version](https://img.shields.io/pypi/v/wads.svg)](https://pypi.org/project/wads/)
[![Python versions](https://img.shields.io/pypi/pyversions/wads.svg)](https://pypi.org/project/wads/)

## What is Wads?

Wads helps you:
- **Create new Python projects** with modern tooling (pyproject.toml, GitHub Actions)
- **Manage CI/CD workflows** with configuration-driven GitHub Actions
- **Handle system dependencies** declaratively in pyproject.toml
- **Migrate legacy projects** from setup.cfg to modern formats
- **Debug CI failures** with automated diagnostics

## Installation

```bash
pip install wads          # light core: config reading + templating engine
pip install wads[create]  # full project-creation / publishing toolchain
pip install wads[all]     # create + docs
```

`wads` ships a **light core** (just enough to read `[tool.wads.ci]` /
`package.json` config and run the templating engine — handy in CI) plus a
heavier `create` extra (`requests`, `build`, `wheel`, `ruamel.yaml`) for
scaffolding and publishing. Use `wads[create]` (or `wads[all]`) when running
`populate`/`pack` to create or publish projects.

## Quick Start

### Create a New Project

```bash
populate my-project --root-url https://github.com/user/my-project
cd my-project
```

This creates a complete project structure with:
- `pyproject.toml` (modern build configuration)
- `README.md`, `LICENSE`, `.gitignore`
- Package directory with `__init__.py`
- GitHub Actions CI/CD workflow (optional)

### Add a frontend component for JS/TS parts (optional)

Python projects often ship a frontend component (a widget, a browser UI, a
TypeScript library). `populate --frontend <profile>` adds a parametrized
**NPM CI** alongside the Python one, following the same
"config-file-driven, fixed-workflow" model. Pick one or more **profiles**:

| Profile | Adds | Subdir | CI |
|---|---|---|---|
| `js` | `package.json` (npm) | `js/` | single-package `npm-ci.yml` |
| `ts` | `package.json` + `tsconfig.json` + `src/index.ts` (tsup build, vitest) | `ts/` | single-package `npm-ci.yml` |
| `ts-monorepo` | pnpm workspace root + `turbo.json` + an example `packages/core` | `ts/` | matrixed `npm-ci-monorepo.yml` |

```bash
# A single TypeScript component:
populate my-project --root-url https://github.com/user/my-project --frontend ts

# Several components at once — each in its own subdir, no workflow collision:
populate my-project --root-url https://github.com/user/my-project --frontend js,ts
```

`--with-npm` is kept as a back-compat alias for `--frontend js`.

Each component gets:
- a `package.json` with a namespaced `"wads"` config block (`wads.ci.*`)
  controlling node versions, lint/test/build commands, and publishing —
  analogous to `[tool.wads.ci]` in `pyproject.toml`;
- a path-filtered `.github/workflows/npm-ci[-<subdir>].yml` stub calling wads's
  reusable NPM workflow (the `js` component keeps the bare `npm-ci.yml`; every
  other component gets `npm-ci-<subdir>.yml`, so multiple components never
  collide).

**Validation runs on every push/PR; publishing is opt-in.** It publishes only
when `wads.ci.publish.enabled` is `true` **and** the commit message contains
the marker **`[publish-npm]`** (deliberately distinct from the Python side).
Publishing uses npm OIDC trusted publishing + provenance by default (no
long-lived token). For a single component, customize the subdirectory and
package name with `--npm-subdir` / `--npm-package-name`.

**Package manager: npm or pnpm.** The single-package reusable workflow drives
**npm** by default and **pnpm** when selected — either explicitly via
`wads.ci.packageManager` (or `populate --npm-package-manager pnpm`) or
auto-detected from a `pnpm-lock.yaml` in the package directory. pnpm consumers
should declare a `"packageManager": "pnpm@x.y.z"` field in their `package.json`
(pnpm's own convention); the CI reads the pnpm version from there. Existing npm
consumers are unaffected (no `pnpm-lock.yaml` → npm). The `ts-monorepo` profile
is pnpm-based by design.

The profile set is extensible: register your own with
`wads.profiles.register_frontend_profile(...)`.

### Configure CI in pyproject.toml

Edit your `pyproject.toml` to configure CI behavior:

```toml
[tool.wads.ci.testing]
python_versions = ["3.10", "3.12"]
pytest_args = ["-v", "--tb=short"]
coverage_enabled = true
test_on_windows = true

[tool.wads.ci.quality.ruff]
enabled = true

[tool.wads.ci.build]
sdist = true
wheel = true

# Opt-in licence gate over the installed dependency closure (default: off).
# See "Licence Perimeter" under CI Configuration Reference.
[tool.wads.licence]
enabled = false
```

The default `ci.yml` is a small stub that calls wads's reusable workflow
(`i2mint/wads/.github/workflows/uv-ci.yml@master`); all behavior is driven by
`[tool.wads.ci.*]` above. Publishing to PyPI happens automatically on your
repo's **default branch** — but only when the Linux test matrix passes.

### Configure Secrets (CI environment variables)

If your tests need API keys or other secrets, declare them once and let wads
wire up both the GitHub Actions transport and the job environment:

```bash
wads-secrets add OPENAI_API_KEY            # env var == GitHub secret name
wads-secrets add HF_TOKEN HF_WRITE_TOKEN    # env var <- differently-named secret
wads-secrets add DATABASE_URL --kind required   # fail CI if the secret is unset
wads-secrets add TEST_LEVEL --variable      # non-sensitive value -> repo variable
wads-secrets list                           # show what's configured
```

`wads-secrets add` (a) records the variable in `[tool.wads.ci.env]` and (b)
runs `gh secret set` (or `gh variable set` with `--variable`) if `gh` is
installed (value taken from `$VAR_NAME` or `--value`). Under the hood there
are two layers: a **transport** — the stub passes your repo's whole secrets
context to the reusable workflow as one `WADS_CI_SECRETS_JSON` secret, so any
secret name works — and an **env policy** (`[tool.wads.ci.env]` —
`required_envvars` / `test_envvars` / `extra_envvars` / `defaults` /
`secret_aliases`) that decides which values become job env vars. Each declared
name resolves against secrets first, then repository *variables* (the right
home for non-sensitive values); committed constants can go straight into
`[tool.wads.ci.env].defaults`. A `required` name that resolves to nothing
fails the build; an undeclared secret is never written to the environment.
Note the JSON transport hands **every** secret the repo can read — including
org-level ones — to the reusable workflow (which only exports the declared
ones). If you want the workflow to receive *only* the names you list, use
`wads-migrate ci-to-stub --transport named` — that mode is limited to the
frozen superset in `wads.ci_secrets.DEFAULT_CI_SECRETS`, and is also the
right choice for orgs with very large shared secrets (the serialized context
must fit in one secret value). Older stubs pass secrets by name the same way;
regenerate with `wads-migrate ci-to-stub` to switch to the JSON transport.

### Declare System Dependencies

Need ffmpeg, ODBC drivers, or other system packages in CI? Declare them in `pyproject.toml`:

```toml
[tool.wads.ops.ffmpeg]
description = "Multimedia framework for video/audio processing"
url = "https://ffmpeg.org/"

check.linux = "which ffmpeg"
check.macos = "which ffmpeg"

install.linux = "sudo apt-get install -y ffmpeg"
install.macos = "brew install ffmpeg"
install.windows = "choco install ffmpeg -y"

note = "Required for audio processing features"
```

The `install-system-deps` action in your CI workflow will automatically install these.

## Core Features

### 1. Project Setup (`populate`)

Create new Python projects with modern best practices:

```bash
# Basic usage
populate my-project

# With custom settings
populate my-project \
  --root-url https://github.com/myorg/my-project \
  --description "My awesome project" \
  --author "Your Name" \
  --license apache
```

**Options:**
- `--root-url`: GitHub repository URL
- `--description`: Project description
- `--author`: Author name
- `--license`: License type (mit, apache, bsd, etc.)
- `--keywords`: Comma-separated keywords
- `--overwrite`: Files to overwrite if they exist

**Tip:** Configure defaults in `wads_configs.json` or use `WADS_CONFIGS_FILE` environment variable to point to your custom config.

### 2. Package and Publish (`pack`)

Build and publish packages to PyPI:

```bash
# See current configuration
pack current-configs

# Increment version and publish
pack go .

# Or step-by-step
pack increment-configs-version
pack run-setup
pack twine-upload-dist
```

### 3. Migration Tools (`wads-migrate`)

Migrate legacy projects to modern format:

```bash
# Migrate setup.cfg to pyproject.toml
wads-migrate setup-to-pyproject setup.cfg -o pyproject.toml

# Migrate old CI workflow to new format
wads-migrate ci-old-to-new .github/workflows/old-ci.yml -o .github/workflows/ci.yml
```

**Python API:**

```python
from wads.migration import migrate_setuptools_to_hatching, migrate_github_ci_old_to_new

# From setup.cfg file
pyproject_content = migrate_setuptools_to_hatching("setup.cfg")

# From setup.cfg dict
config = {"metadata": {"name": "myproject", "version": "1.0.0"}}
pyproject_content = migrate_setuptools_to_hatching(config)

# Migrate CI workflow
new_ci = migrate_github_ci_old_to_new(".github/workflows/ci.yml")
```

### 4. CI Debugging (`wads-ci-debug`)

Diagnose and fix GitHub Actions CI failures:

```bash
# Analyze latest failure
wads-ci-debug myorg/myrepo

# Analyze specific run
wads-ci-debug myorg/myrepo --run-id 1234567890

# Generate fix instructions
wads-ci-debug myorg/myrepo --fix --local-repo .
```

The tool will:
- Fetch CI logs from GitHub
- Parse test failures and errors
- Identify root causes
- Generate fix instructions with file locations and suggested changes

## CI Configuration Reference

Wads uses `pyproject.toml` as a single source of truth for CI configuration. Here's what you can configure:

### Install Extras

By default CI installs only your package's core dependencies. If your test suite
needs an extra (e.g. a heavier `create`/`dev` group), declare it so CI installs
`.[extras]`:

```toml
[tool.wads.ci.install]
extras = "dev"          # or a list, e.g. ["dev", "test"]
```

### Python Versions and Testing

```toml
[tool.wads.ci.testing]
python_versions = ["3.10", "3.11", "3.12"]  # Test matrix
pytest_args = ["-v", "--tb=short"]           # Pytest arguments
coverage_enabled = true                      # Enable coverage
coverage_threshold = 80                      # Minimum coverage %
exclude_paths = ["examples", "scrap"]        # Paths to exclude
test_on_windows = true                       # Run Windows tests
```

### Code Quality Tools

```toml
[tool.wads.ci.quality.ruff]
enabled = true
# line_length = 88

[tool.wads.ci.quality.mypy]
enabled = false
# strict = true
```

### Custom Commands

```toml
[tool.wads.ci.commands]
pre_test = [
    "python scripts/setup_test_data.py",
]
post_test = [
    "python scripts/cleanup.py",
]
```

### Build and Publish

```toml
[tool.wads.ci.build]
sdist = true
wheel = true

[tool.wads.ci.publish]
enabled = true  # Publish to PyPI on main/master
```

### Licence Perimeter (`wads-licence-check`)

Fails the build when the **installed dependency closure** carries a licence the
project's policy forbids — copyleft (GPL / AGPL / LGPL), or source-available /
non-commercial (SSPL, BUSL, Elastic-2.0, RAIL, CC-BY-NC).

It walks the *transitive* closure, not just the declared list, because that is
where the exposures actually hide, and it reads a distribution's declaration
through a **precision ladder** — PEP 639 `License-Expression`, then the
`License ::` trove classifiers, then the **first line** of the free-text
`License` field. No single field is enough: `click` declares an expression and
no classifiers, `i2` declares neither and only a free-text field, and a
whole-field substring scan flags BSD-3-Clause numpy as copyleft because its
field carries an LGPL URL for a vendored notice.

Run it anywhere:

```bash
wads-licence-check                                    # this project
wads-licence-check path/to/project --json             # for a fleet sweep
wads-licence-check . --python .venv/bin/python        # read another env
```

The CI gate is **opt-in**. A repo that declares nothing sees no change:

```toml
[tool.wads.licence]
enabled = true                    # default false: opt in per repo
include-extras = []               # [] = hard deps only; ["*"] = every extra
unknown-is-failure = true         # a blank licence field is *unaudited*, not fine
unclassified-is-failure = false   # e.g. MPL-2.0: reported, does not fail

[tool.wads.licence.exceptions]
certifi = "MPL-2.0 — weak, file-level, over an unmodified CA bundle. Audited 2026-08."
```

That is the whole configuration most repos need. The `allowed` / `forbidden`
pattern lists are deliberately absent from it: **they REPLACE the defaults, they
do not extend them**, so writing them out by hand is a narrowing unless the list
is a superset of what ships. If you do set them, write them as TOML **literal**
strings — single quotes — because a basic string processes escapes and turns
`"\bGPL"` into a backspace character followed by `GPL`, which matches nothing:

```toml
[tool.wads.licence]
# Start from wads.licence_check.DFLT_FORBIDDEN and ADD, rather than replacing:
forbidden = [
  '\bAGPL',
  '\bAffero\b',
  '\bGPL(?![\w.+-]*\s+with\b)',
  '\bGNU General Public\b',
  '\bLGPL',
  '\bLesser General Public\b',
  '\bLibrary General Public\b',
  '\bNethack General Public\b',
  '\bEUPL\b',
  '\bBusiness Source\b',
  '\bBUSL\b',
  '\bSSPL\b',
  '\bElastic[- ]?(2\.0|License|v2)\b',
  '(?:\b|-)(?:open)?rail(?:-m)?\b',
  '\bCC[- ]BY[- ]NC\b',
  '\bNon[- ]?Commercial\b',
  '\bProprietary\b',
  '\bYourOwnAddition\b',   # the point: ADD to the defaults, never restate a subset
]
```

Exceptions take either the terse map above or an array-of-tables, which is the
shape to reach for when the decision needs a record behind it:

```toml
[[tool.wads.licence.exceptions]]
dependency = "PyGithub"
licence = "LGPL (classifier only; no SPDX expression published)"
scope = "core"
decided = "2026-08-30"
decided_in = "https://github.com/thorwhalen/hubcap/issues/10"
reason = """
Accepted, not removable: PyGithub's objects are this package's values, so there
is no honest "core without it" to install. Consumed by ordinary import, neither
vendored nor patched, so the LGPL relink freedom is intact.
"""
```

Note the table lives under `[tool.wads]`, not `[tool.wads.ci]`: the policy is a
fact about the package, and the tool is useful outside CI. Only `enabled` is a
CI concern.

Exit codes are `0` (holds), `1` (breached) and `2` (the tool could not run —
bad config, unreadable environment, a policy that cannot detect).

#### The self-check

Every run first proves the live policy still catches known-copyleft
declarations *and* still clears known-permissive ones, and refuses to report at
all if it cannot — a detector nobody has demonstrated is a detector nobody has
checked.

The bar is per licence FAMILY, caught whole or not at all. Permitting a family
outright is a coherent stance (LGPL for dynamically linked libraries is the
usual one) and stays expressible; it is then named in every run's output, so
"PERIMETER HOLDS" is never read as "there is no LGPL in here". Catching *part*
of a family is refused, because it is never a stance — it is the bug. A gate
whose LGPL pattern ended in `\b` caught the legacy `GNU Library or Lesser`
classifier and missed `LGPLv2`, `LGPLv2+`, `LGPLv3` and `LGPLv3+`, i.e. every
modern spelling, while reporting itself as working.

#### What counts as a failure

`forbidden` and a blank declaration (`unknown-is-failure`, default on) fail, and
so does a **declared dependency that is not installed** in the environment being
read: that is a piece of the perimeter nobody looked at, and it is the same
confident-green failure as reading the wrong environment. The one exception is a
requirement gated on an environment marker (`tomli; python_version < "3.11"`),
which is reported as `NOT APPLICABLE` and does not fail.

For the same reason the tool refuses to run at all on a project whose
`[project].dependencies` is absent or listed in `dynamic` — an empty closure it
could not read is not an empty closure. Write `dependencies = []` if a project
genuinely has none.

## System Dependencies

System dependencies are declared using the `[tool.wads.ops.*]` format and automatically installed in CI via the `install-system-deps` action.

**Format:**

```toml
[tool.wads.ops.{package-name}]
description = "Description of the package"
url = "https://package-homepage.com"

# Check if already installed (exit code 0 = present)
check.linux = "which package-name"
check.macos = "brew list package-name"
check.windows = "where package-name"

# Install commands (string or list of strings)
install.linux = "sudo apt-get install -y package-name"
install.macos = "brew install package-name"
install.windows = "choco install package-name -y"

# Optional metadata
note = "Additional installation notes"
alternatives = ["alternative-package"]
```

**Real-world example (ODBC drivers):**

```toml
[tool.wads.ops.unixodbc]
description = "ODBC driver interface for database connectivity"
url = "https://www.unixodbc.org/"

check.linux = "dpkg -s unixodbc || rpm -q unixODBC"
check.macos = "brew list unixodbc"

install.linux = [
    "sudo apt-get update",
    "sudo apt-get install -y unixodbc unixodbc-dev"
]
install.macos = "brew install unixodbc"

note = "On Alpine: apk add unixodbc unixodbc-dev"
alternatives = ["iodbc"]
```

See [docs/SYSTEM_DEPENDENCIES.md](docs/SYSTEM_DEPENDENCIES.md) for comprehensive examples.

## Claude Code Skills

Wads ships with [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skills for AI-assisted workflows. Install them globally so they're available in every project:

```bash
wads-install-skills
```

This symlinks skills to `~/.claude/skills/`, so they stay in sync when wads is updated:

| Command | Description |
|---------|-------------|
| `/setup-py-project` | AI-guided Python project creation: name suggestions, PyPI/GitHub availability checking, repo creation, file population |
| `/wads-migrate` | Migrate projects to modern wads setup (pyproject.toml + uv CI) |

**Example:**
```
/setup-py-project "a tool for audio signal processing"
```

To list available skills without installing: `wads-install-skills --list`
To update existing skills: `wads-install-skills --force`

## Documentation

- **[System Dependencies Guide](misc/docs/SYSTEM_DEPENDENCIES.md)** - `[tool.wads.ops.*]` format and examples
- **[Migration Guide](misc/docs/MIGRATION.md)** - Migrate from setup.cfg to pyproject.toml
- **[Utilities Reference](misc/docs/UTILITIES.md)** - CLI tools (`wads-ci-debug`, `wads-migrate`)
- **[CLAUDE.md](CLAUDE.md)** - AI agent guide for working with this project

## Troubleshooting

### Version Tag Misalignment

If PyPI publishing fails with "appears to already exist":

```
WARNING  Skipping mypackage-0.1.4-py3-none-any.whl because it appears to already exist
```

This means your git tags are misaligned with the version in `pyproject.toml`.

**Fix:**

1. Check the current PyPI version: https://pypi.org/project/your-package/
2. Update `version` in `pyproject.toml` to a higher number
3. Create and push git tag:
   ```bash
   git tag 0.1.5
   git push origin 0.1.5
   ```

### CI Failures

Use `wads-ci-debug` to analyze failures:

```bash
wads-ci-debug myorg/myrepo --fix
```

Common issues:
- Missing system dependencies → Add to `[tool.wads.ops.*]`
- Python version incompatibilities → Check `python_versions` in `[tool.wads.ci.testing]`
- Test failures → Review generated fix instructions

## Development

### Running Tests

```bash
pytest wads/tests/
```

### Building Documentation

```bash
pip install -e ".[docs]"
epythet build
```

## License

Apache Software License 2.0

## Links

- **PyPI:** https://pypi.org/project/wads/
- **GitHub:** https://github.com/i2mint/wads
- **Issues:** https://github.com/i2mint/wads/issues
