Skip to content

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:

  1. Run just codegen to generate latest Pydantic models
  2. Find the generated model in src/pypropertyme/codegen/models.py
  3. 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
  1. Create client subclass in src/pypropertyme/client.py:
class Tenancies(Client):
    model = Tenancy
    endpoint_path = "tenancies"

Pull Request Process

  1. Fork the repository and create a feature branch
  2. Make your changes following the code style guidelines
  3. Add tests for new functionality
  4. Run the test suite: just test
  5. Run linting: just lint
  6. Update documentation if needed
  7. 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 formatting
  • pyupgrade - Python syntax modernization
  • uv-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