Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ jobs:
run: |
mypy --strict tests/type_checks/

- name: Enforce adopter type-check fixture contract
run: |
python scripts/check_type_ignore_contract.py

- name: Run tests
run: |
pytest tests/ -v --cov=src/adcp --cov-report=term-missing
Expand Down
15 changes: 14 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Pre-commit hooks for AdCP Python client
# See https://pre-commit.com for more information
# Installation: uv add --dev pre-commit && uv run pre-commit install
# Installation: make bootstrap

repos:
# Black code formatting
Expand Down Expand Up @@ -30,6 +30,19 @@ repos:
types: [python]
pass_filenames: false
args: [src/adcp]
- id: adopter-type-checks
name: adopter type-check fixtures
entry: uv run --extra dev mypy
language: system
types: [python]
pass_filenames: false
args: [--strict, tests/type_checks/]
- id: adopter-type-ignore-contract
name: "adopter type-check fixtures contain no type: ignore suppressions"
entry: uv run python scripts/check_type_ignore_contract.py
language: system
types: [python]
pass_filenames: false
- id: check-commit-msg
name: release-please commit subject check
entry: scripts/check-commit-msg.sh
Expand Down
17 changes: 10 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,19 @@ Wrong order creates invalid escape sequences!

## Pre-Commit Checks

Run these three checks locally before every commit — they mirror CI exactly:
Run these checks locally before every commit:

```bash
ruff check src/ # Linter
mypy src/adcp/ # Type checker
pytest tests/ -v # Tests
make lint
make typecheck-all
make test
```

All three must pass. CI runs them across Python 3.10–3.13; locally running on your current version catches most issues.
All must pass. `make ci-local` runs the core local gate: lint, all type-check
contracts, tests, and generated-code validation. Specialized CI jobs such as
storyboard runners, Postgres conformance, and conventional-commit validation
still run separately in GitHub Actions. CI runs the core matrix across Python
3.10–3.13; locally running on your current version catches most issues.

## Parallel Agent Isolation (git worktrees)

Expand All @@ -216,8 +220,7 @@ git worktree add /tmp/claude-issue-<N>-<slug> -b claude/issue-<N>-<slug> main
```bash
cd /tmp/claude-issue-<N>-<slug>
cp "$(git rev-parse --git-common-dir)/../.env" .env # .env is not inherited
pre-commit install # hooks are not inherited from parent worktree
pip install -e .[dev] # install in this worktree's context
make bootstrap # requires uv; installs deps and hooks here
```

**Teardown (after branch is merged):**
Expand Down
49 changes: 32 additions & 17 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Thank you for your interest in contributing to the AdCP Python client!

## Development Setup

This repository expects `uv` on your `PATH` for the local contributor
environment because the pre-commit hooks run through `uv run` to match CI
dependencies.

1. Clone the repository:
```bash
git clone https://github.com/adcontextprotocol/adcp-client-python.git
Expand All @@ -12,42 +16,51 @@ cd adcp-client-python

2. Install dependencies and pre-commit hooks:
```bash
pip install -e ".[dev]"
pre-commit install
pre-commit install --hook-type commit-msg
make bootstrap
```

3. Run tests:
```bash
pytest
make test
```

4. Format code:
```bash
black src/ tests/
ruff check src/ tests/ --fix
make format
make lint
```

5. Type check:
```bash
mypy src/
make typecheck-all
```

For the core local CI-style pass before opening a PR, run:

```bash
make ci-local
```

This covers lint, all type-check contracts, tests, and generated-code
validation. GitHub Actions still runs specialized jobs such as storyboard
runners, Postgres conformance, and conventional-commit validation.

## Project Structure

```
src/adcp/
├── __init__.py # Main exports
├── client.py # ADCPClient & ADCPMultiAgentClient
├── protocols/
│ ├── base.py # Protocol interface
│ ├── a2a.py # A2A adapter
│ └── mcp.py # MCP adapter
├── types/
│ ├── core.py # Core types
│ └── tools.py # Generated from AdCP schema
└── utils/
└── operation_id.py # Utilities
├── canonical_formats/ # Canonical format fixtures and adapters
├── compat/ # Legacy protocol compatibility adapters
├── decisioning/ # DecisioningPlatform framework
├── protocols/ # A2A and MCP client adapters
├── server/ # Server framework, auth, routing, middleware
├── signing/ # Request signing, verification, JWKS, replay stores
├── testing/ # In-process test helpers and test agents
├── types/ # Public types, generated models, mypy plugin
├── utils/ # Shared helpers
└── validation/ # Schema validation hooks and loaders
```

## Guidelines
Expand All @@ -68,7 +81,9 @@ src/adcp/
### Type Safety
- All functions must have type hints
- Use Pydantic for data validation
- Run `mypy` before committing
- Run `make typecheck-all` before committing
- `tests/type_checks/` is the adopter-facing type contract suite. Fixtures must
pass `mypy --strict` without `# type: ignore` suppressions.

### Documentation
- Add docstrings to all public functions
Expand Down
32 changes: 27 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help format lint typecheck test test-type-checks regenerate-schemas pre-push ci-local clean install-dev check-schema-drift
.PHONY: help check-uv bootstrap format lint lint-all typecheck typecheck-all test test-type-checks check-type-ignore-contract regenerate-schemas pre-push ci-local clean install-dev check-schema-drift

# Detect Python and use venv if available
PYTHON := $(shell if [ -f .venv/bin/python ]; then echo .venv/bin/python; else echo python3; fi)
Expand All @@ -7,28 +7,46 @@ PYTEST := $(shell if [ -f .venv/bin/pytest ]; then echo .venv/bin/pytest; else e
BLACK := $(shell if [ -f .venv/bin/black ]; then echo .venv/bin/black; else echo black; fi)
RUFF := $(shell if [ -f .venv/bin/ruff ]; then echo .venv/bin/ruff; else echo ruff; fi)
MYPY := $(shell if [ -f .venv/bin/mypy ]; then echo .venv/bin/mypy; else echo mypy; fi)
UV := uv

help: ## Show this help message
@echo 'Usage: make [target]'
@echo ''
@echo 'Available targets:'
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'

install-dev: ## Install package in development mode with dev dependencies
install-dev: ## Legacy pip-only install; bootstrap is preferred for contributors
$(PIP) install -e ".[dev]"

check-uv:
@command -v $(UV) >/dev/null || { \
echo "uv is required for bootstrap and pre-commit hooks. Install it from https://docs.astral.sh/uv/"; \
exit 1; \
}

bootstrap: check-uv ## Recommended contributor setup: uv-managed deps and git hooks
$(UV) run --extra dev --group dev pre-commit install
$(UV) run --extra dev --group dev pre-commit install --hook-type commit-msg

format: ## Format code with black (excludes generated files)
$(BLACK) src/ tests/ scripts/
@echo "✓ Code formatted successfully (_generated.py excluded via pyproject.toml)"

lint: ## Run linter (ruff) on source code
$(RUFF) check src/ tests/
$(RUFF) check src/
@echo "✓ Linting passed"

lint-all: ## Run linter (ruff) on source and tests
$(RUFF) check src/ tests/
@echo "✓ Source and test linting passed"

typecheck: ## Run type checker (mypy) on source code
$(MYPY) src/adcp/
@echo "✓ Type checking passed"

typecheck-all: typecheck test-type-checks check-type-ignore-contract ## Run all type-check contracts
@echo "✓ All type-check contracts passed"

test: ## Run test suite with coverage
$(PYTEST) tests/ -v --cov=src/adcp --cov-report=term-missing
@echo "✓ All tests passed"
Expand All @@ -41,6 +59,10 @@ test-type-checks: ## Run adopter-pattern type-check suite (mypy --strict, zero t
$(MYPY) --strict tests/type_checks/
@echo "✓ Adopter type-checks passed"

check-type-ignore-contract: ## Fail if adopter type-check fixtures use type: ignore suppressions
$(PYTHON) scripts/check_type_ignore_contract.py
@echo "✓ Adopter type-check fixtures contain no type: ignore suppressions"

test-generation: ## Run only code generation tests
$(PYTEST) tests/test_code_generation.py -v
@echo "✓ Code generation tests passed"
Expand Down Expand Up @@ -70,15 +92,15 @@ validate-generated: ## Validate generated code (syntax and imports)
@$(PYTHON) -m py_compile src/adcp/types/_generated.py
@echo "✓ Generated code validation passed"

pre-push: format lint typecheck test validate-generated ## Run all checks before pushing (format, lint, typecheck, test, validate)
pre-push: format lint typecheck-all test validate-generated ## Run all checks before pushing (format, lint, typecheck, test, validate)
@echo ""
@echo "================================"
@echo "✓ All pre-push checks passed!"
@echo "================================"
@echo ""
@echo "Safe to push to remote."

ci-local: lint typecheck test validate-generated ## Run CI checks locally (without formatting)
ci-local: lint typecheck-all test validate-generated ## Run core CI checks locally (without formatting)
@echo ""
@echo "================================"
@echo "✓ All CI checks passed!"
Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1461,21 +1461,29 @@ client = ADCPMultiAgentClient.from_env()

## Development

This repository uses `uv` for the contributor environment and pre-commit hooks.
Install `uv` first if it is not already on your `PATH`.

```bash
# Install with dev dependencies
pip install -e ".[dev]"
# Install dev dependencies and git hooks (requires uv)
make bootstrap

# Run tests
pytest
make test

# Type checking
mypy src/
# Type checking: source package plus adopter-facing type fixtures
make typecheck-all

# Format code
black src/ tests/
ruff check src/ tests/
make format
make lint
```

`make ci-local` runs the core local gate: lint, all type-check contracts, tests,
and generated-code validation. Specialized CI jobs such as storyboard runners,
Postgres conformance, and conventional-commit validation still run separately in
GitHub Actions.

## Contributing

Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. All contributors must agree to the [AgenticAdvertising.Org IPR Policy](https://github.com/adcontextprotocol/adcp/blob/main/IPR_POLICY.md) — the bot prompts new contributors on their first PR and a single signature covers all AAO repositories.
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"pytest-cov>=4.0.0",
"mypy>=1.20.0,<1.21",
"mypy==1.20.2",
"black>=23.0.0",
"ruff>=0.1.0",
# Pin to exact version: codegen's variant numbering (e.g. CreateMediaBuyResponse1 vs
Expand All @@ -148,6 +148,7 @@ dev = [
# propagate. Required at runtime by examples/v3_reference_seller/
# src/app.py and seed.py.
"alembic>=1.13.0",
"pre-commit>=4.4.0",
]
docs = [
"pdoc3>=0.10.0",
Expand Down Expand Up @@ -225,6 +226,8 @@ python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
warn_unreachable = true
strict_equality_for_none = true
# adcp.types.SchemaVariant — see adcp.types.mypy_plugin for the marker
# semantics. Adopters that want the same override-compat behavior on
# their own cross-class field overrides should add this plugin to their
Expand Down Expand Up @@ -321,5 +324,5 @@ dev = [
# from CI's, producing a different error count than CI on the same
# source — and a dead-weight hook that everyone bypasses with
# ``SKIP=mypy``.
"mypy>=1.20.0",
"mypy==1.20.2",
]
46 changes: 46 additions & 0 deletions scripts/check_type_ignore_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Fail if adopter type-check fixtures rely on type-ignore suppressions."""

from __future__ import annotations

import sys
import tokenize
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
TYPE_CHECK_DIR = ROOT / "tests" / "type_checks"


def type_ignore_comments(path: Path) -> list[tuple[int, str]]:
findings: list[tuple[int, str]] = []
with path.open("rb") as handle:
for token in tokenize.tokenize(handle.readline):
if token.type != tokenize.COMMENT:
continue
comment = token.string.lstrip("#").strip()
if comment.startswith("type: ignore"):
findings.append((token.start[0], token.string.strip()))
return findings


def main() -> int:
failures: list[str] = []
for path in sorted(TYPE_CHECK_DIR.rglob("*.py")):
for line_no, comment in type_ignore_comments(path):
rel_path = path.relative_to(ROOT)
failures.append(f"{rel_path}:{line_no}: {comment}")

if not failures:
return 0

print(
"tests/type_checks/ fixtures must pass mypy --strict without type-ignore suppressions.",
file=sys.stderr,
)
for failure in failures:
print(failure, file=sys.stderr)
return 1


if __name__ == "__main__":
raise SystemExit(main())
4 changes: 2 additions & 2 deletions src/adcp/adagents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,7 +1310,7 @@ def _is_bare_authorized_agent_entry(agent: dict[str, Any]) -> bool:


def _build_domain_index(
properties: list[dict[str, Any]],
properties: list[Any],
) -> dict[str, list[dict[str, Any]]]:
"""Build a ``publisher_domain → [property, ...]`` index.

Expand Down Expand Up @@ -1933,7 +1933,7 @@ class AuthorizationContext:
raw_properties: Raw property data from adagents.json
"""

def __init__(self, properties: list[dict[str, Any]]):
def __init__(self, properties: list[Any]):
"""Initialize from list of properties.

Args:
Expand Down
2 changes: 0 additions & 2 deletions src/adcp/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,6 @@ def supports_v3(self) -> bool:
Returns:
True if major_versions includes 3.
"""
if self._caps.adcp is None:
return False
for v in self._caps.adcp.major_versions:
if (v.root if hasattr(v, "root") else v) == 3:
return True
Expand Down
Loading
Loading