From 013a806a4e3fad061006767bf4211aa861a3137d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 6 Nov 2025 12:56:19 -0500 Subject: [PATCH 1/2] fix: actually use PYPY_API_TOKEN secret for PyPI publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous revert commit didn't actually change the secret name. The correct secret name in the repository is PYPY_API_TOKEN. This should fix the 403 Forbidden error when publishing to PyPI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/release-please.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index dd21e2d..89324ba 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -43,5 +43,5 @@ jobs: if: ${{ steps.release.outputs.release_created }} env: TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + TWINE_PASSWORD: ${{ secrets.PYPY_API_TOKEN }} run: twine upload dist/* From 9aea9a3414ac8ee4d127a1a1da077d33a12d4d8e Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 6 Nov 2025 13:04:37 -0500 Subject: [PATCH 2/2] docs: add CLAUDE.md with Python SDK development learnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document key learnings from building the Python AdCP SDK: **Type Safety & Code Generation:** - Auto-generate Pydantic models from upstream schemas - Handle missing schema types with documented type aliases - Use TYPE_CHECKING for optional dependencies - Use cast() for JSON deserialization type safety **Testing Strategy:** - Mock at the right level (_get_client() not httpx class) - httpx response.json() is SYNCHRONOUS not async - Test the API as it exists, not as we wish it existed **CI/CD & Release:** - Verify secret names before changing them (PYPY_API_TOKEN not PYPI_API_TOKEN) - Release Please automates version bumps and PyPI publishing - Entry points in pyproject.toml enable uvx usage **Python Patterns:** - String escaping order matters (backslash first, then quotes) - Atomic file operations for config files - Connection pooling for HTTP clients - Python 3.10+ required for | union syntax 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..05abf77 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,126 @@ +# Python SDK Development Learnings + +## Type Safety & Code Generation + +**Auto-generate from specs when possible** +- Download schemas from canonical source (e.g., adcontextprotocol.org/schemas) +- Generate Pydantic models automatically - keeps types in sync with spec +- Validate generated code in CI (syntax check + import test) +- For missing upstream types, add type aliases with clear comments explaining why + +**Handling Missing Schema Types** +When schemas reference types that don't exist upstream: +```python +# MISSING SCHEMA TYPES (referenced but not provided by upstream) +# These types are referenced in schemas but don't have schema files +FormatId = str +PackageRequest = dict[str, Any] +``` + +**Type Checking Best Practices** +- Use `TYPE_CHECKING` for optional dependencies to avoid runtime import errors +- Use `cast()` for JSON deserialization to satisfy mypy's `no-any-return` checks +- Add specific `type: ignore` comments (e.g., `# type: ignore[no-any-return]`) rather than blanket ignores +- Test type checking in CI across multiple Python versions (3.10+) + +## Testing Strategy + +**Mock at the Right Level** +- For HTTP clients: Mock `_get_client()` method, not the httpx class directly +- For async operations: Use `AsyncMock` for async functions, `MagicMock` for sync methods +- Remember: httpx's `response.json()` is SYNCHRONOUS, not async + +**Test API Changes Properly** +- When API changes from kwargs to typed objects, update tests to match +- Remove tests for non-existent methods rather than keep failing tests +- Test the API as it exists, not as we wish it existed + +## CI/CD & Release Automation + +**GitHub Actions Secrets** +- Secret names matter! Check actual secret name in repository settings +- Common pattern: `PYPY_API_TOKEN` (not `PYPI_API_TOKEN`) for PyPI publishing +- Test locally with `python -m build` before relying on CI + +**Release Please Workflow** +- Runs automatically on push to main +- Creates release PR with version bump and changelog +- When release PR is merged, automatically publishes to PyPI +- Requires proper `[project.scripts]` entry point in pyproject.toml for CLI tools + +**Entry Points for CLI Tools** +```toml +[project.scripts] +toolname = "package.__main__:main" +``` +This enables `uvx toolname` and `pip install toolname` to work correctly. + +## Python-Specific Patterns + +**Optional Dependencies with TYPE_CHECKING** +```python +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from optional_lib import SomeType + +try: + from optional_lib import SomeType as _SomeType + AVAILABLE = True +except ImportError: + AVAILABLE = False +``` + +**Atomic File Operations** +For config files with sensitive data: +```python +temp_file = CONFIG_FILE.with_suffix(".tmp") +with open(temp_file, "w") as f: + json.dump(config, f, indent=2) +temp_file.replace(CONFIG_FILE) # Atomic rename +``` + +**Connection Pooling** +```python +# Reuse HTTP client across requests +self._client: httpx.AsyncClient | None = None + +async def _get_client(self) -> httpx.AsyncClient: + if self._client is None: + limits = httpx.Limits( + max_keepalive_connections=10, + max_connections=20, + ) + self._client = httpx.AsyncClient(limits=limits) + return self._client +``` + +## Common Pitfalls to Avoid + +**String Escaping in Code Generation** +Always escape in this order: +1. Backslashes first: `\\` → `\\\\` +2. Then quotes: `"` → `\"` +3. Then control chars (newlines, tabs) + +Wrong order creates invalid escape sequences! + +**Python Version Requirements** +- Union syntax `str | None` requires Python 3.10+ +- Always include `from __future__ import annotations` at top of files +- Use `target-version = "py310"` in ruff/black config +- Test in CI across all supported Python versions + +**Test Fixtures vs. Mocks** +- Don't over-mock - it hides serialization bugs +- Test actual API calls when possible +- Use real Pydantic validation in tests +- Mock external services, not internal logic + +## Additional Important Reminders + +**NEVER**: +- Assume a "typo" without checking the actual secret name in GitHub settings + +**ALWAYS**: +- Verify secret names match repository settings before "fixing" them