Contributing¶
Thank you for your interest in contributing to PyPropertyMe! This guide covers the development setup and contribution process.
Development Setup¶
Prerequisites¶
- Python 3.13+
- uv for package management
- just for task automation
- Docker (for Kiota client generation)
Getting Started¶
# Clone the repository
git clone https://github.com/garyj/pypropertyme.git
cd pypropertyme
# Bootstrap the project (creates .env, installs deps)
just bootstrap
# Or just install dependencies
just install
Environment Setup¶
Copy the environment template and add your credentials:
cp .env.template .env
Edit .env with your PropertyMe API credentials.
Project Structure¶
src/pypropertyme/
├── base.py # BasePMeModel class (Pydantic ↔ Kiota bridge)
├── models.py # Custom Pydantic models
├── client.py # High-level client with entity clients
├── auth.py # OAuth 2.0 authentication
├── cli.py # CLI tool
├── exceptions.py # Custom exceptions
├── codegen/ # Auto-generated Pydantic models (DO NOT EDIT)
│ └── models.py
├── api/ # Auto-generated Kiota code (DO NOT EDIT)
│ ├── api_client.py
│ └── models/
└── sync/ # Optional sync modules
├── airtable/
└── mongodb/
Generated Code
Files in src/pypropertyme/api/ and src/pypropertyme/codegen/ are auto-generated. Do not edit them directly.
Development Commands¶
Code Quality¶
# Format code with ruff
just format
# Lint code with ruff
just lint
Testing¶
# Run all tests
just test
# Run tests excluding slow Airtable tests
just testfast
# Run integration tests only
just test-integration
Code Generation¶
# Full regeneration: download OpenAPI spec, convert to v3, generate clients
just generate
# Generate Pydantic models from OpenAPI spec
just codegen
Documentation¶
# Serve documentation locally
just docs
# Build documentation
just docs-build
Code Style¶
Formatting¶
- Line length: 119 characters
- Quote style: Single quotes
- Formatter: ruff
Docstrings¶
Use Google-style docstrings:
def get(self, entity_id: str) -> Model:
"""Fetch a single entity by ID.
Args:
entity_id: The unique identifier of the entity.
Returns:
The entity model with full details.
Raises:
APIError: If the entity is not found (404).
Example:
>>> contact = await client.contacts.get("abc-123")
>>> print(contact.name_text)
"""
Type Hints¶
All public functions and methods should have type hints:
async def all(self) -> list[Contact]:
...
async def get(self, entity_id: str) -> ContactDetail:
...
Working with Models¶
When adding support for new PropertyMe entities:
- Run
just codegento generate latest Pydantic models - Find the generated model in
src/pypropertyme/codegen/models.py - Create custom model in
src/pypropertyme/models.py:
# In models.py
from pypropertyme.codegen.models import TenancyApiData
from pypropertyme.api.models.tenancy_api_data import TenancyApiData as KiotaTenancyApiData
class Tenancy(TenancyApiData):
_kiota_model = KiotaTenancyApiData
# All fields inherited from TenancyApiData
- Create client subclass in
src/pypropertyme/client.py:
class Tenancies(Client):
model = Tenancy
endpoint_path = "tenancies"
Pull Request Process¶
- Fork the repository and create a feature branch
- Make your changes following the code style guidelines
- Add tests for new functionality
- Run the test suite:
just test - Run linting:
just lint - Update documentation if needed
- Submit a pull request with a clear description
Commit Messages¶
Use conventional commit format:
feat: add support for documents endpoint
fix: handle 404 errors in contacts.get()
docs: update API reference
refactor: simplify pagination logic
test: add tests for property filters
PR Description¶
Include:
- Summary of changes
- Related issue numbers
- Testing notes
- Breaking changes (if any)
Testing¶
Running Tests¶
# All tests
just test
# Specific test file
uv run pytest tests/test_client.py -v
# Specific test
uv run pytest tests/test_client.py::test_contacts_all -v
Test Markers¶
# Skip integration tests
uv run pytest -m "not integration"
# Skip slow Airtable tests
uv run pytest -m "not airtable"
Writing Tests¶
import pytest
from pypropertyme.client import Client
@pytest.mark.asyncio
async def test_contacts_all(mock_client):
contacts = await mock_client.contacts.all()
assert len(contacts) > 0
assert all(hasattr(c, 'id') for c in contacts)
Pre-commit Hooks¶
The project uses pre-commit hooks:
ruff- Linting and formattingpyupgrade- Python syntax modernizationuv-lock- Keep uv.lock in sync
Run manually:
pre-commit run --all-files
Questions?¶
- Open an issue for bugs or feature requests
- Check existing issues before creating new ones
- Be respectful and constructive in discussions