CLAUDE.md - Python IPAM Project¶
Claude Code-specific configuration and standards for the Python IPAM project.
Documentation Standards¶
IMPORTANT LANGUAGE REQUIREMENT: - ALL documentation MUST be written in English - This includes: README.md, API.md, FEATURES.md, REFACTORING.md, CLAUDE.md, code comments, commit messages - German or other languages are NOT permitted in documentation
DOCUMENTATION UPDATE POLICY:
- Every code change MUST be evaluated for documentation impact
- Before committing ANY change, ask: "Does this require documentation updates?"
- Documentation files to check:
- README.md - User-facing documentation, setup instructions
- API.md - REST API endpoint documentation
- FEATURES.md - Feature tracking and roadmap
- REFACTORING.md - Architecture and refactoring notes
- CLAUDE.md - This file, project standards
- Documentation updates should be committed together with code changes
- Never commit code without updating related documentation
Release Guardrails¶
- Do not tag or create a release unless the latest CI build is successful.
- Before merging the release-please PR: check GitHub's Security → Code scanning tab for new CRITICAL/HIGH Trivy findings since the last release.
- Fix available (dependency bump, base image update) → apply as a normal
fix/dockercommit first, let it flow into the same or next release. - No fix available / false positive → dismiss in the GitHub UI with a reason ("no fix available", "not affected") instead of ignoring it, so it doesn't stay open indefinitely.
- Claude Code must not merge a release PR while an unaddressed CRITICAL finding is open – flag it to the user first.
Testing Expectations¶
- Add or update tests for new features and behavior changes.
- Tests must never touch
./ipam.db.tests/conftest.pysetsDATABASE_URL=sqlite:///:memory:beforeipamis imported, because the engine is bound insidecreate_app(); changingapp.config["SQLALCHEMY_DATABASE_URI"]afterwards has no effect. Usecreate_app(name, {"SQLALCHEMY_DATABASE_URI": ...})when a test needs its own database file.
Project Overview¶
Python IPAM is a modern IP Address Management (IPAM) system based on: - Flask 3.0 (Python Web Framework) - SQLite/SQLAlchemy (Database) - Bootstrap 5 + DataTables (Frontend) - pytest (Testing Framework) - Docker (Containerization)
Code Style Rules¶
Python (Google Style Guide)¶
Formatter/Linter: Ruff, 80 character line length
(config in pyproject.toml)
Naming Conventions:
- Modules/Packages: lowercase_with_underscores
- Classes: CapWords (e.g., NetworkForm, HostModel)
- Functions/Variables: lowercase_with_underscores
- Constants: CAPS_WITH_UNDERSCORES
- Private: _leading_underscore
Import Order (separated by blank lines):
from __future__ import annotations
import os
import ipaddress
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from ipam import db, Network, Host
Dockerfile (Hadolint Standard)¶
Follow security best practices:
FROM python:3.15-slim
# Security: Non-root user
RUN useradd -r -s /bin/false ipam
USER ipam
# Layer optimization
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# JSON notation for CMD
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
Git Commits (Angular Convention)¶
Format:
Types for this project:
- feat: New IPAM features (Host/Network management)
- fix: Bug fixes in web UI or API
- deps: Dependency updates (generated by Renovate)
- test: Unit tests for models/routes/forms
- docker: Container configuration
- docs: README or API documentation
Examples:
feat(networks): add VLAN ID support
fix(hosts): resolve auto-detection for overlapping subnets
test(models): add network cascade delete tests
docker: optimize layer caching in Dockerfile
docs: update pyenv setup instructions
IMPORTANT: Do not use AI signatures in commits!
Shell/Bash¶
For test scripts and setup automation:
#!/bin/bash
# Functions
function setup_venv() {
local python_version="$1"
echo "Setting up Python ${python_version}..."
}
# Prefer [[ ]]
if [[ -d "venv" ]]; then
source venv/bin/activate
fi
# Quote variables
echo "PATH: ${PATH}"
Project-Specific Standards¶
Testing¶
Test Structure:
tests/
├── conftest.py # Pytest fixtures (Application Factory)
├── test_models.py # SQLAlchemy Model Tests
├── test_routes.py # Flask Route Tests
├── test_forms.py # WTForms Validation Tests
├── test_database.py # Database initialization tests
├── test_export_import.py # Export/Import plugin tests
├── test_crud_operations.py # CRUD operation tests
└── test_address_assignment.py # Assignment/lease/observation model
Test Commands:
# All tests
pytest -v
# With coverage
pytest --cov=ipam --cov-report=html
# Specific tests
pytest tests/test_models.py::TestNetworkModel::test_network_properties
# Database tests
pytest tests/test_database.py -v
Database Models¶
SQLAlchemy models in ipam/models.py:
from ipam.extensions import db
class Network(db.Model):
__tablename__ = 'networks' # Explicit table names
# Primary Keys as first column
id = db.Column(db.Integer, primary_key=True)
# Relationships with cascade definitions
hosts = db.relationship('Host', backref='network_ref',
cascade='all, delete-orphan')
# Properties for calculated values
@property
def total_hosts(self):
return len(list(ipaddress.IPv4Network(
f"{self.network}/{self.cidr}", strict=False
).hosts()))
Flask Application Structure¶
Application Factory Pattern in ipam/__init__.py:
from ipam.extensions import db
from ipam.config import config
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
db.init_app(app)
from ipam.web import web_bp
from ipam.api import api_bp
app.register_blueprint(web_bp)
app.register_blueprint(api_bp)
return app
Flask Routes¶
Web Routes in ipam/web/routes.py:
from ipam.web import web_bp
from ipam.extensions import db
from ipam.models import Network, Host
@web_bp.route('/')
def index():
"""Dashboard."""
networks = Network.query.all()
return render_template('index.html', networks=networks)
API Routes in ipam/api/networks.py:
from flask_restx import Namespace, Resource
from ipam.extensions import db
from ipam.models import Network
api = Namespace('networks', description='Network operations')
@api.route('/')
class NetworkList(Resource):
def get(self):
"""List all networks."""
networks = Network.query.all()
return {'data': [network.to_dict() for network in networks]}
Templates¶
HTML/Jinja2 template standards:
- Bootstrap 5 CSS classes
- Responsive design
- DataTables for tables
- Bootstrap Icons (bi-*)
MCP Servers¶
This project is configured with MCP servers via .mcp.json:
- context7: fetch up-to-date documentation for Flask, SQLAlchemy, and other libraries instead of relying on potentially outdated training knowledge
- playwright: browser automation for end-to-end testing of the web UI
- sqlite: direct inspection of
./ipam.db(schema, ad-hoc debug queries)
On first use in Claude Code, approve the servers when prompted (/mcp shows
connection status).
Environment & Tools¶
Local Development¶
pyenv Setup (pyenv + pyenv-virtualenv):
Reads the Python version (requires-python) and project name from
pyproject.toml, installs the interpreter, creates/migrates the
python-ipam-env virtualenv, runs pyenv local, and installs
requirements.txt + requirements-dev.txt. Re-run it after bumping
requires-python.
Manual equivalent:
pyenv install 3.15
pyenv virtualenv 3.15 python-ipam-env
pyenv local python-ipam-env # writes .python-version (the env name), auto-activates
pip install -r requirements.txt -r requirements-dev.txt
Database Initialization:
# Create database (on first start)
python3 -c "from ipam import create_app; from ipam.extensions import db; app = create_app(); app.app_context().push(); db.create_all()"
Development Server:
python app.py # Flask dev server on :5000
# Available URLs:
# - Web Interface: http://localhost:5000
# - REST API: http://localhost:5000/api/v1
# - Swagger UI: http://localhost:5000/api/v1/docs
Mobile/iPhone Access (for testing responsive UI):
Provides a temporary public URL pointing to the local dev server.Docker Development¶
Development Container:
Production Container:
Quality Assurance¶
Linting/Formatting (local):
# Python Code (requirements-dev.txt: ruff, pytest-cov)
ruff check .
ruff format --check .
# Dockerfile
hadolint Dockerfile
# Shell Scripts
shellcheck run_tests.sh
Testing:
Pre-Commit Quality Checklist¶
- [ ]
ruff check .clean? - [ ]
ruff format --check .clean? - [ ]
pytest -v– all tests passing? - [ ] No
*.dbfile committed? - [ ] No API key / secret in code?
- [ ] Conventional Commit format followed?
- [ ] No AI signature in the commit?
- [ ] Documentation checked: does
README.md,API.md,FEATURES.md, orCLAUDE.md/AGENTS.mdneed updating for this change?
API Documentation¶
REST API Base URL: /api/v1
Swagger UI: http://localhost:5000/api/v1/docs
Main Endpoints:
Networks:
- GET /api/v1/networks - List all networks (with filtering/pagination)
- POST /api/v1/networks - Create network
- GET /api/v1/networks/{id} - Get network
- PUT /api/v1/networks/{id} - Update network
- DELETE /api/v1/networks/{id} - Delete network
Hosts:
- GET /api/v1/hosts - List all hosts (with filtering/pagination)
- POST /api/v1/hosts - Create host
- GET /api/v1/hosts/{id} - Get host
- PUT /api/v1/hosts/{id} - Update host
- DELETE /api/v1/hosts/{id} - Delete host (unconditional manual operation)
- POST /api/v1/hosts/{id}/retire-lease - Atomic conditional retirement of an expired
dynamic collector record; full expected snapshot required, see API.md. Server
minimum grace: LEASE_RETIRE_MIN_AGE_SECONDS (3600 default). No scheduler enabled.
- POST /api/v1/hosts/{id}/release-reservation - Atomic conditional delete or
conversion to dynamic of a reservation removed at its source; full expected
snapshot required, see API.md.
IP Management:
- GET /api/v1/ip/networks/{id}/next-ip - Get next free IP
- GET /api/v1/ip/networks/{id}/available-ips - List all free IPs
- GET /api/v1/ip/{ip_address} - Query IP status
Response Format:
{
"data": [...],
"pagination": {
"page": 1,
"per_page": 50,
"total_items": 100,
"total_pages": 2
}
}
Complete documentation: API.md
Deployment¶
Container Images¶
Production Images (Chainguard distroless):
- Registry: ghcr.io/tuxpeople/python-ipam
- Base: cgr.dev/chainguard/python:latest (distroless; versioned tags
need a Chainguard subscription, so we track latest)
- Security: 0 CRITICAL/HIGH vulnerabilities (Trivy scanned)
- Size: ~50-100MB (multi-stage build)
- User: nonroot (UID 65532)
- Python: 3.15 (from latest; keep pyproject.toml requires-python in step)
Tags:
- latest - Always points to latest stable release
- 1.0.0 - Specific patch version
- 1.0 - Minor version (updates with new patches)
- 1 - Major version (updates with new minors/patches)
All tags are created by the release.yml image job after a release-please
PR merge – a regular main push does not produce a new image (see
## Releases above).
Multi-stage Build:
# Stage 1: Build dependencies in -dev image
FROM cgr.dev/chainguard/python:latest-dev AS build
RUN python -m venv /tmp/venv && \
/tmp/venv/bin/pip install --upgrade 'pip<25.2' setuptools wheel
# Stage 2: Install Python packages
FROM build AS build-venv
COPY requirements.txt /tmp/requirements.txt
RUN /tmp/venv/bin/pip install -r /tmp/requirements.txt
# Stage 3: Minimal runtime image (distroless)
FROM cgr.dev/chainguard/python:latest
COPY --from=build-venv /tmp/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN mkdir -p /tmp && chmod 1777 /tmp
Kubernetes (if used):
# Follow Kubernetes best practices
metadata:
labels:
app.kubernetes.io/name: python-ipam
app.kubernetes.io/version: "1.0.0"
app.kubernetes.io/component: backend
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
Troubleshooting¶
Common Issues:
-
SQLite Lock Issues:
-
pyenv Python not found:
-
Docker Build Failures:
Maintenance¶
Dependencies Update:
Update the pinned version inrequirements.txt (runtime, shipped in the
image) or requirements-dev.txt (tests/lint) by hand. Do not use
pip freeze > requirements.txt: it would copy dev tools into the image.
Renovate normally proposes these bumps.
Database Migrations (on schema changes):
# In Python Console
from ipam import create_app
from ipam.extensions import db
app = create_app()
with app.app_context():
db.drop_all() # Caution in production!
db.create_all()
Database Initialization (Command line):
python3 -c "from ipam import create_app; from ipam.extensions import db; app = create_app(); app.app_context().push(); db.create_all()"
Release Process¶
Semantic Versioning¶
This project follows Semantic Versioning 2.0.0:
Version Format: MAJOR.MINOR.PATCH
- MAJOR: Breaking changes (API changes, database schema changes)
- MINOR: New features (backwards compatible)
- PATCH: Bug fixes (backwards compatible)
Examples:
- v1.0.0 - First production release
- v1.1.0 - New features (e.g., subnet calculator)
- v1.1.1 - Bug fixes
- v2.0.0 - Breaking changes (e.g., database schema migration)
Creating a Release¶
Automated via release-please –
no manual git tag or gh release create anymore.
Flow:
1. Merge Conventional-Commit-formatted commits (feat, fix, ...) to main
as usual (PR or direct commit).
2. release-please opens/updates a release PR (chore(main): release X.Y.Z)
with an auto-generated changelog preview.
3. Merge that PR when you want to ship a release. This automatically:
- Creates the Git tag vX.Y.Z and a GitHub Release with categorized
changelog
- Triggers the image job in release.yml (Docker image + SBOM +
Trivy scan, pushed to GHCR with X.Y.Z, X.Y, X, latest tags)
4. No manual tag, no manual release notes, no manual Docker verification
step – all of the above happens automatically from the merge.
One-time setup (already done for this repo, see below):
- Repo-Setting Settings → Actions → General → Workflow permissions →
"Allow GitHub Actions to create and approve pull requests" enabled
- Settings → General → Pull Requests → "Allow merge commits" disabled
(squash-merge only) – otherwise the release-please changelog gets
duplicate entries
- release-please runs with a GitHub App token (actions/create-github-app-token),
not GITHUB_TOKEN: release PRs opened with GITHUB_TOKEN need manual
approval before their checks run. The App (Contents, Pull requests, Issues:
read & write; no webhook) is installed only on the repos that use
release-please (python-ipam, dorflade-mhd); each workflow's token is scoped
to its own repository. Repo variable
RELEASE_APP_CLIENT_ID and secret RELEASE_APP_PRIVATE_KEY (a key used only
by this repo). To rotate: create a new key in the App settings, update the
secret, delete the old key.
Version bump mapping: feat → Minor, fix/deps/docker/perf → Patch,
feat! or BREAKING CHANGE: footer → Major. Config: release-please-config.json,
current version tracked in .release-please-manifest.json.
Hotfix Release Process¶
No special process needed – a fix: commit on main is picked up by
release-please like any other change. Merge the resulting release PR to ship
it immediately.
CI/CD Integration¶
Two workflows, split by cost – cheap checks on every push, the expensive build only on release:
tests.yml(every push/PR tomain/develop):lint(Ruff check+format),pytest(tests+coverage),hadolint(Dockerfile),image-scan(fast, single-arch Trivy scan, no push) – all four run in parallel.release.ymlcalls the reusable workflowsrelease-please.ymlandcontainer-release.ymlfromtuxpeople/shared, pinned by SHA (push tomainmaintains the release-please PR; only merging it, or manualworkflow_dispatch, runs thetestsandimagejobs):tests: the completetests.ymlas safety net; a failure blocks the image- Multi-arch Docker build (
linux/amd64,linux/arm64), pushed to GHCR - Image signed with cosign (keyless, by digest)
- Cosign keyless signature, verified right after signing. The certificate
names
container-release.ymlintuxpeople/shared, see the header of that file for thecosign verifycommand - SBOM generation (Trivy, SPDX) – signed attestation in the registry
(
actions/attest, together with the build provenance) and uploaded to the GitHub Release; verify withgh attestation verify oci://ghcr.io/tuxpeople/python-ipam:latest --owner tuxpeople - Trivy scan: SARIF upload to Code scanning (informational); the blocking
scan is
image-scanintests.yml(only findings with an available fix) - Tags:
X.Y.Z,X.Y,X,latest(read fromversion.txt, which release-please'ssimplerelease-type maintains)
A regular main push therefore does not produce a new image – only a
merged release PR does. See ## Releases above for the full flow.
Last Update: 2025-10-03 Maintainer: Python IPAM Team