|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## What This Project Is |
| 6 | + |
| 7 | +`pulp_python` is a Pulp plugin that enables self-hosted, PyPI-compatible Python package repositories. It is a Django application that integrates with [pulpcore](https://github.com/pulp/pulpcore) via the Pulp plugin API. Key capabilities: sync from PyPI, upload packages, serve via pip, pull-through caching, versioned repos, PEP 740 attestations. |
| 8 | + |
| 9 | +## Development Commands |
| 10 | + |
| 11 | +### Linting |
| 12 | +```bash |
| 13 | +# Code formatting check |
| 14 | +black --check --diff . |
| 15 | + |
| 16 | +# Style/docstring linting |
| 17 | +flake8 |
| 18 | + |
| 19 | +# Run both via CI scripts |
| 20 | +.ci/scripts/extra_linting.sh |
| 21 | +.ci/scripts/check_pulpcore_imports.sh |
| 22 | +``` |
| 23 | + |
| 24 | +**Style rules** (`.flake8`): max line length 100, black-compatible, docstrings required on public methods (with several D-codes ignored — see `.flake8`). |
| 25 | + |
| 26 | +### Testing |
| 27 | + |
| 28 | +Tests require a running Pulp instance. Functional tests use pytest fixtures from `pulp_python/pytest_plugin.py` and connect to a live API. |
| 29 | + |
| 30 | +```bash |
| 31 | +# Run all tests |
| 32 | +pytest pulp_python/tests/ |
| 33 | + |
| 34 | +# Run only unit tests (no Pulp instance needed) |
| 35 | +pytest pulp_python/tests/unit/ |
| 36 | + |
| 37 | +# Run a single functional test file |
| 38 | +pytest pulp_python/tests/functional/api/test_sync.py |
| 39 | + |
| 40 | +# Run a single test by name |
| 41 | +pytest pulp_python/tests/functional/api/test_sync.py::test_name |
| 42 | +``` |
| 43 | + |
| 44 | +### Building |
| 45 | +```bash |
| 46 | +# Build the Python distribution wheel |
| 47 | +python -m build |
| 48 | + |
| 49 | +# Install in development mode |
| 50 | +pip install -e . |
| 51 | +``` |
| 52 | + |
| 53 | +### Changelog Entries |
| 54 | + |
| 55 | +New changelog fragments go in `CHANGES/` using towncrier format. See `pyproject.toml` `[tool.towncrier]` for configuration. |
| 56 | + |
| 57 | +## Architecture |
| 58 | + |
| 59 | +### Plugin Structure |
| 60 | + |
| 61 | +This follows the standard Pulp plugin pattern. The plugin registers itself via the entry point `pulpcore.plugin` → `pulp_python.app.PulpPythonPluginAppConfig`. |
| 62 | + |
| 63 | +All application code lives in `pulp_python/app/`: |
| 64 | + |
| 65 | +| File/Dir | Purpose | |
| 66 | +|---|---| |
| 67 | +| `models.py` | Django models for all content types and repository objects | |
| 68 | +| `serializers.py` | DRF serializers with validation and metadata extraction | |
| 69 | +| `viewsets.py` | DRF viewsets implementing the REST API | |
| 70 | +| `urls.py` | URL routing — PyPI API endpoints | |
| 71 | +| `utils.py` | Metadata extraction, PyPI template rendering, canonicalization | |
| 72 | +| `provenance.py` | PEP 740 attestation/provenance validation logic | |
| 73 | +| `tasks/` | Celery async tasks (sync, publish, upload, repair, vulnerability) | |
| 74 | +| `pypi/` | PyPI-specific views (Simple API, Legacy upload, Metadata, Provenance) | |
| 75 | +| `settings.py` | Django settings additions | |
| 76 | +| `migrations/` | Database migrations | |
| 77 | + |
| 78 | +### Core Models |
| 79 | + |
| 80 | +- **`PythonPackageContent`** — the main content type; stores all Python package metadata (PEP 440/core-metadata fields) plus release info (filename, sha256, size). Unique per `(sha256, _pulp_domain)`. |
| 81 | +- **`PackageProvenance`** — PEP 740 provenance objects linked to a `PythonPackageContent`. |
| 82 | +- **`PythonRepository`** — Repository; supports `autopublish`, `PULL_THROUGH_SUPPORTED = True`. Calls `tasks.publish()` on new versions if `autopublish` is set. |
| 83 | +- **`PythonRemote`** — Remote with package filtering (`includes`, `excludes`, `package_types`, `keep_latest_packages`, `exclude_platforms`, `prereleases`, `provenance`). |
| 84 | +- **`PythonPublication`** — Publication for generated PyPI Simple API index files. |
| 85 | +- **`PythonDistribution`** — Distribution serving content via `content_handler()` which generates JSON API responses and handles Simple API serving from remote storage. |
| 86 | +- **`NormalizeName`** — A custom Django `Transform` that normalizes package names per PEP 426 (regex replace `., _, -` → `-`, lowercased). Used as `name__normalize=value` in queries. |
| 87 | + |
| 88 | +### PyPI API Endpoints (`urls.py`) |
| 89 | + |
| 90 | +``` |
| 91 | +pypi/<domain>/<path>/legacy/ → UploadView (twine/pip upload) |
| 92 | +pypi/<domain>/<path>/integrity/... → ProvenanceView (PEP 740) |
| 93 | +pypi/<domain>/<path>/pypi/<meta>/ → MetadataView (JSON API) |
| 94 | +pypi/<domain>/<path>/simple/... → SimpleView (pip simple index) |
| 95 | +``` |
| 96 | + |
| 97 | +### Async Tasks (`tasks/`) |
| 98 | + |
| 99 | +- **`sync.py`** — Syncs packages from a remote (PyPI or compatible). Uses `bandersnatch` and `pypi-simple`. |
| 100 | +- **`publish.py`** — Creates a `PythonPublication` with Simple API index files. |
| 101 | +- **`upload.py`** — Handles package uploads (`upload` for single, `upload_group` for multi-upload). |
| 102 | +- **`repair.py`** — Re-downloads missing artifacts. |
| 103 | +- **`vulnerability_report.py`** — Fetches vulnerability data for repository content. |
| 104 | + |
| 105 | +### Test Fixtures (`pytest_plugin.py`) |
| 106 | + |
| 107 | +The pytest plugin defines session-scoped and function-scoped fixtures for all major objects: `python_repo_factory`, `python_remote_factory`, `python_distribution_factory`, `python_publication_factory`, `python_content_factory`, `python_repo_with_sync`. Functional tests depend on a running Pulp instance configured via environment. |
| 108 | + |
| 109 | +### Key Conventions |
| 110 | + |
| 111 | +- The plugin is domain-compatible (`domain_compatible = True`). All content models have a `_pulp_domain` FK. |
| 112 | +- Package name lookups always use the `NormalizeName` transform: `name__normalize=canonicalize_name(name)`. |
| 113 | +- Content deduplication is enforced in `PythonRepository.finalize_new_version()` via pulpcore's `remove_duplicates()`. |
| 114 | +- Access policies use `AutoAddObjPermsMixin` and role-based permissions defined in each model's `Meta.permissions`. |
| 115 | +- Changelog entries use towncrier; fragment files belong in `CHANGES/`. |
0 commit comments