Metadata-Version: 2.5
Name: celery-audit
Version: 0.1.0
Summary: Static, deterministic audit of a Celery configuration
Project-URL: Homepage, https://github.com/software-family/celery-audit
Project-URL: Repository, https://github.com/software-family/celery-audit
Project-URL: Issues, https://github.com/software-family/celery-audit/issues
Project-URL: Changelog, https://github.com/software-family/celery-audit/blob/main/CHANGELOG.md
Author-email: Elie Terrien <contact@elie-terrien.fr>
License-Expression: MIT
License-File: LICENSE
Keywords: audit,celery,configuration,linter,static-analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: System :: Systems Administration
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# celery-audit

[![CI](https://github.com/software-family/celery-audit/actions/workflows/ci.yml/badge.svg)](https://github.com/software-family/celery-audit/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)

Static, deterministic audit of a Celery configuration.

Reads your configuration and task declarations with `ast` — it never imports the audited code,
never connects to a broker, and never talks to a worker. Safe to run in CI on an untrusted
checkout.

The report is designed to be consumed by an LLM: stable rule IDs, stable ordering, no timestamps
and no absolute paths, so the same input always produces a byte-identical report. When a value
cannot be resolved statically, the tool says so explicitly instead of assuming a default.

## Installation

```bash
pip install celery-audit
```

Python 3.11 or later, and **no runtime dependencies** — installing the auditor never installs
Celery. Note that the audit parses with the grammar of the interpreter it runs on, so install it
on one at least as new as the project you are auditing; a file it cannot parse is reported as
`CEL000` rather than skipped.

## Usage

```bash
celery-audit <source-root> --config <path-to-config-module>
```

The configuration module is named explicitly — nothing is auto-discovered. The source root is
scanned for task declarations and call sites.

**Django splits its Celery configuration across two files**, so pass both, in the order they take
effect:

```bash
celery-audit . --config config/settings/base.py --config config/celery_app.py \
               --settings-namespace CELERY
```

`settings.py` holds the namespaced `CELERY_*` settings; `celery_app.py` holds the `app.conf.*`
writes and `autodiscover_tasks()`. Auditing only one of them reports absences the other file had
already filled in. A later module overrides an earlier one.

| Option | Default | Meaning |
|---|---|---|
| `--config` | required | Configuration module to audit. **Repeatable**, in the order the modules take effect. |
| `--settings-namespace` | none | Django-style prefix, e.g. `CELERY` to read `CELERY_TASK_SERIALIZER` |
| `--format text\|json` | `text` | `json` is the machine contract |
| `--fail-on critical\|high\|medium\|low` | `high` | Lowest severity that sets a non-zero exit code |
| `--disable` | none | Comma-separated rule IDs to skip; they are listed in the report |
| `--no-practice` | off | Skip best-practice rules and report only defects; skipped rules are listed |

Exit codes: `0` nothing at or above the threshold, `1` at least one such finding, `2` the audit
could not run (config module missing or unparsable, bad arguments).

**`unknown` findings never affect the exit code.** They mean the audit could not decide, which is
not grounds to break a build — but they are always counted in the summary, so a blind audit cannot
pass for a clean one.

Example:

```bash
celery-audit . --config celeryconfig.py --format json --fail-on critical
```

## Project settings

Declare the defaults once in `pyproject.toml` and the CI command stays `celery-audit .`:

```toml
[tool.celery-audit]
config = ["config/settings/base.py", "config/celery_app.py"]  # or a single string
namespace = "CELERY"       # Django-style prefix, optional
format = "text"
fail_on = "high"
disable = ["CEL015"]
practice = true            # set false to report only defects
```

Command line options win over these. An unknown key or an invalid value is refused rather than
ignored — a typo silently dropped would leave you believing you had configured something.

## Silencing a finding

Put the marker on **the line the report points at**, naming the rules to silence:

```python
accept_content = ["pickle"]  # celery-audit: ignore[CEL001]
```

The marker names its rules on purpose. A blanket "ignore everything" would also hide the findings
you never considered, and suppressed findings are counted in the report so a project cannot look
clean for a reason nobody can see.

An `unknown` finding cannot be suppressed: hiding "the audit could not decide" is hiding a blind
spot, which is the one thing this tool exists to make visible.

A suppression that silences nothing is reported as `CEL098` — including one whose rule ID is
misspelled, which would otherwise sit there looking effective forever.

## Explaining a rule

```bash
celery-audit --explain CEL012
```

Prints what the rule checks and why. No project needed.

## What it checks

**[`RULES.md`](RULES.md) explains every rule**: what it reads, why it reports it, an example of the
violation, the fix, and what it deliberately stays silent about.

| ID | Severity | Check |
|---|---|---|
| `CEL001` | critical | `accept_content` allows pickle |
| `CEL002` | critical | `task_serializer` / `result_serializer` is pickle |
| `CEL003` | critical | Broker or backend URL carries a hard-coded password |
| `CEL004` | medium | Task has no time limit anywhere |
| `CEL005` | medium | Soft time limit at or above the hard one, so it can never fire |
| `CEL006` | medium | `time_limit=0` silently disables the global default |
| `CEL007` | high | Late acks without `worker_prefetch_multiplier=1` |
| `CEL008` | medium | Automatic retries with no retry ceiling |
| `CEL009` | medium | Automatic retries with no backoff |
| `CEL010` | medium | `autoretry_for` catches `Exception` |
| `CEL011` | high | Result expiry switched off (`result_expires = None`) |
| `CEL012` | high | Redis/SQS visibility timeout below the longest task |
| `CEL013` | medium | `task_routes` entry matching no task |
| `CEL014` | high | Task or call site using a queue absent from `task_queues` |
| `CEL015` | low | Task with no explicit `name` |
| `CEL016` | high | Task in a module no declared import reaches |
| `CEL017` | high | Beat entry naming a task that does not exist |
| `CEL018` | medium | Beat period at or below the task's time limit |
| `CEL019` | low | Startup retries silently re-enabled by Celery 6 |
| `CEL020` | high | `task_always_eager` runs every task inline |
| `CEL021` | critical | Undeclared queue while `task_create_missing_queues` is off |
| `CEL022` | high | Task dispatched inside a database transaction |
| `CEL023` | medium | Beat entries scheduled with no `timezone`, so they run in UTC |
| `CEL024` | medium | Tasks declare a time limit while `worker_prefetch_multiplier` is unset |
| `CEL025` | low | No `result_backend`, so task results are discarded |
| `CEL026` | low | Beat entry with no `expires` |

Findings come in two categories. **Defects** are things that are wrong. **Suggestions** are
practices that will hurt when something specific happens — they are `low` or `medium`, so with
the default `--fail-on high` they never break a build. `--no-practice` reports defects only; an
internal project wanting to enforce the suggestions sets `--fail-on low`.

`CEL000` reports a file the audit could not parse, `CEL098` a suppression that silences nothing, and
`CEL099` a designated config module in which no Celery setting was recognised — so neither a blind
spot nor dead configuration is ever silent.

## Development

```bash
uv sync              # install
uv run pytest        # tests
uv run ruff check .  # lint
uv run mypy          # types (strict)
```

Two invariants govern every change here, and both are held by tests rather than by review: the
audit never imports the code it reads, and the same input always produces a byte-identical report.
[`CONTRIBUTING.md`](CONTRIBUTING.md) says what that means for a patch.

Released versions are listed in [`CHANGELOG.md`](CHANGELOG.md).
