# Contributing to ai4gh

Thank you for your interest in contributing to ai4gh. This guide covers the development setup, coding conventions, and submission process.

## Development Setup

### Prerequisites

- Python >= 3.10
- [uv](https://docs.astral.sh/uv/) (recommended) or pip

### Clone and Install

```bash
git clone https://github.com/susheel/ai4gh.git
cd ai4gh

# Install with dev dependencies
uv sync --extra dev

# Verify installation
uv run pytest
uv run ai4gh --help
```

### Running Quality Checks

```bash
# Run all tests
uv run pytest

# Run with coverage
uv run pytest --cov=ai4gh --cov-report=term-missing

# Lint
uv run ruff check .

# Auto-fix lint issues
uv run ruff check --fix .

# Type check
uv run mypy src/
```

## Project Structure

```
src/ai4gh/
  core/           # Config, auth, transport, plugin system
  specs/           # Per-spec plugins
    beacon/        # Each spec has: client.py, models.py, __init__.py
    drs/
    ...
  mcp/             # MCP server (tools, resources, prompts)
  compliance/      # Compliance test suites + report generation
  cli/             # Typer CLI
```

## Coding Conventions

### Python Style

- Use `from __future__ import annotations` in every module
- Use Pydantic v2 for all data models
- All network operations must be async (httpx)
- Use `str | None` instead of `Optional[str]`
- Follow existing import ordering (ruff I001)

### Data Models

All models inherit from `GA4GHBase`:

```python
from ai4gh.core.models import GA4GHBase


class MyModel(GA4GHBase):
    field: str
    optional_field: str | None = None
```

`GA4GHBase` is configured with `extra="allow"` to handle forward-compatible spec extensions.

### API Clients

All clients inherit from `GA4GHClient`:

```python
from ai4gh.core.client import GA4GHClient


class MyClient(GA4GHClient):
    spec_name = "my-spec"
    spec_version = "1.0.0"
    base_path = "/api/v1"

    async def get_something(self, id: str) -> MyModel:
        data = await self._get(f"/things/{id}")
        return MyModel.model_validate(data)
```

### MCP Tools

MCP tool handlers follow this pattern:

```python
async def _my_tool(arguments: dict) -> str:
    from ai4gh.specs.my_spec.client import MyClient

    base_url = arguments.get("base_url", "")
    async with MyClient(base_url=base_url) as client:
        result = await client.get_something(arguments["id"])
    return result.model_dump_json()
```

- Use lazy imports inside the handler
- Use the client as an async context manager
- Return JSON strings

### Compliance Suites

Register suites using the decorator:

```python
from ai4gh.compliance.runner import (
    ComplianceSuite,
    TestResult,
    TestStatus,
    register_suite,
)


@register_suite("my-spec")
class MySpecSuite(ComplianceSuite):
    spec_name = "my-spec"
    spec_version = "1.0.0"

    async def test_service_info(self) -> TestResult:
        import httpx

        async with httpx.AsyncClient() as client:
            resp = await client.get(f"{self.base_url}/service-info")
        if resp.status_code == 200:
            return TestResult(
                name="test_service_info",
                status=TestStatus.PASS,
                spec=self.spec_name,
            )
        return TestResult(
            name="test_service_info",
            status=TestStatus.FAIL,
            message=f"Expected 200, got {resp.status_code}",
            spec=self.spec_name,
        )
```

## Testing

### Writing Tests

- Place unit tests in `tests/unit/test_{module}/`
- Use `pytest-httpx` for mocking HTTP requests
- Use `pytest-asyncio` for async tests (mode=auto is configured)
- Test file naming: `test_{feature}.py`

### Test Example

```python
from ai4gh.specs.drs.models import DRSObject


class TestDRSObject:
    def test_minimal(self) -> None:
        obj = DRSObject(id="abc123", self_uri="drs://example/abc123")
        assert obj.id == "abc123"

    def test_with_checksums(self) -> None:
        obj = DRSObject(
            id="abc123",
            self_uri="drs://example/abc123",
            checksums=[{"checksum": "abc", "type": "md5"}],
        )
        assert len(obj.checksums) == 1
```

## Adding a New Spec Plugin

1. Create `src/ai4gh/specs/{spec_name}/` with:
   - `__init__.py` - exports
   - `models.py` - Pydantic models from the GA4GH schema
   - `client.py` - async client inheriting `GA4GHClient`

2. Register as a plugin in `pyproject.toml`:
   ```toml
   [project.entry-points."ai4gh.plugins"]
   my_spec = "ai4gh.specs.my_spec"
   ```

3. Add pip extra:
   ```toml
   [project.optional-dependencies]
   my-spec = []  # Add spec-specific deps if needed
   ```

4. Add MCP tool in `src/ai4gh/mcp/tools.py`

5. Add compliance suite in `src/ai4gh/compliance/suites/{spec_name}.py`

6. Write tests in `tests/unit/test_{spec_name}/`

7. Write quickstart guide in `docs/quickstart/{spec_name}.md`

## Pull Request Process

1. Create a feature branch from `main`
2. Make your changes following the conventions above
3. Ensure all checks pass: `uv run pytest && uv run ruff check . && uv run mypy src/`
4. Write a clear PR description explaining the change
5. Link any related issues

## License

By contributing, you agree that your contributions will be licensed under the Apache-2.0 License.
