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:
# 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]Import Architecture for Generated Types The type system has a strict layering to prevent brittleness:
generated_poc/*.py (internal, auto-generated from schemas)
↓
_generated.py (internal consolidation)
↓
stable.py + aliases.py + _ergonomic.py (public API / internal infrastructure)
↓
__init__.py (user-facing exports)
Only these modules may import from generated_poc/ or _generated.py:
stable.py: Re-exports base types with clean namesaliases.py: Creates semantic aliases for numbered discriminated union types_ergonomic.py: Applies BeforeValidator coercion for type ergonomics
All other source code should import from adcp.types (the public API).
Type Checking Best Practices
- Use
TYPE_CHECKINGfor optional dependencies to avoid runtime import errors - Use
cast()for JSON deserialization to satisfy mypy'sno-any-returnchecks - Add specific
type: ignorecomments (e.g.,# type: ignore[no-any-return]) rather than blanket ignores - Test type checking in CI across multiple Python versions (3.10+)
Mock at the Right Level
- For HTTP clients: Mock
_get_client()method, not the httpx class directly - For async operations: Use
AsyncMockfor async functions,MagicMockfor 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
GitHub Actions Secrets
- Secret names matter! Check actual secret name in repository settings
- Common pattern:
PYPY_API_TOKEN(notPYPI_API_TOKEN) for PyPI publishing - Test locally with
python -m buildbefore 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
[project.scripts]
toolname = "package.__main__:main"This enables uvx toolname and pip install toolname to work correctly.
Optional Dependencies with TYPE_CHECKING
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 = FalseAtomic File Operations For config files with sensitive data:
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 renameConnection Pooling
# 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._clientString Escaping in Code Generation Always escape in this order:
- Backslashes first:
\\→\\\\ - Then quotes:
"→\" - Then control chars (newlines, tabs)
Wrong order creates invalid escape sequences!
Python Version Requirements
- Union syntax
str | Nonerequires Python 3.10+ - Always include
from __future__ import annotationsat 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
NEVER:
- Assume a "typo" without checking the actual secret name in GitHub settings
ALWAYS:
- Verify secret names match repository settings before "fixing" them