Skip to content

Contributing to Google Suite

Thanks for your interest in contributing! Here's how to get started.

Development Setup

Prerequisites

Clone and Install

git clone https://github.com/PabloAlaniz/google-suite.git
cd google-suite

# Creates .venv with every package, extra and dev tool pinned by uv.lock
uv sync --all-extras

CI installs from the same uv.lock. If you change dependencies in pyproject.toml, run uv lock and commit the updated lockfile.

Running Tests

# Run all tests
uv run pytest

# Run with coverage
uv run pytest --cov --cov-report=html

# Run one package's tests
uv run pytest packages/gmail/tests/
uv run pytest api/tests/

# Run a single test
uv run pytest packages/gmail/tests/test_query.py::TestQueryBuilder::test_from_query

Test module names must be unique across the repo (test_gmail_client.py, not test_client.py); collection fails otherwise.

Integration tests

tests/integration/ runs real flows (create, read, delete) against a Google account. They are not part of uv run pytest and skip themselves unless enabled. Use a throwaway account:

gsuite auth login --force --scopes all     # with the test account
GSUITE_INTEGRATION=1 uv run pytest tests/integration -rs

Every test deletes what it creates. Tasks and Contacts tests skip when the token lacks their scopes. In CI, integration.yml runs them weekly and on demand with the GSUITE_INTEGRATION_TOKEN secret (the output of gsuite auth export) in the integration environment. OAuth apps in "Testing" status issue refresh tokens that expire after 7 days.

Linting and Type Checking

# Check code style
uv run ruff check packages/ api/ cli/ scripts/

# Auto-fix issues and format
uv run ruff check --fix packages/ api/ cli/ scripts/
uv run ruff format packages/ api/ cli/ scripts/

# mypy error counts may only go down (see mypy-baseline.json)
uv run python scripts/mypy_ratchet.py
# after fixing type errors:
uv run python scripts/mypy_ratchet.py --update

CI

Every PR runs lint, the mypy ratchet, a build/install smoke test and the lowest-supported dependency versions. Package tests run per package on Python 3.11 (oldest supported) and 3.14 (latest), but only for the packages a PR touches (core or shared config runs all of them). macOS and Windows run the full suite. Nightly runs add the latest release of every dependency.

The agent skill

skills/gsuite-sdk/ is an Agent Skill: SKILL.md (setup, CLI, a short SDK tour, errors) plus one file per service in references/. Agents run its snippets as written, so cli/tests/test_skill_md.py checks it with agent-skill-check: the Agent Skills spec, every Python call, CLI command and option, and every GSUITE_* variable against the code. A renamed method or flag fails CI, so update the skill in the same PR that changes the API.

The same folder reaches several indexes (see the playbook, which the author's other projects follow too):

  • ClawHub: published with the SDK's version by the Release workflow (skill job).
  • skills.sh: npx skills add PabloAlaniz/google-suite.
  • Claude Code: .claude-plugin/ makes the repo a marketplace (claude plugin marketplace add PabloAlaniz/google-suite); claude-plugins.dev indexes it.
  • Gemini CLI: gemini-extension.json + the gemini-cli-extension topic.

release-please bumps the version in .claude-plugin/plugin.json and gemini-extension.json. To preview a ClawHub publish:

npx clawhub@0.23.3 skill publish skills/gsuite-sdk --slug gsuite-sdk --version X.Y.Z --dry-run

The MCP server

packages/mcp is a uv workspace member published as its own distribution, gsuite-mcp (it depends on gsuite-sdk), so MCP clients can run uvx gsuite-mcp. The release builds both (uv build --all-packages), uploads both to PyPI, and then publishes server.json to the official MCP Registry (mcp-registry job, GitHub OIDC). The registry checks the mcp-name comment in packages/mcp/README.md, which is gsuite-mcp's PyPI description. release-please keeps the versions of gsuite-mcp and server.json in step with gsuite-sdk.

Documentation

The site at https://pabloalaniz.github.io/google-suite/ is built with MkDocs from docs/, the READMEs (root and packages) and the docstrings:

uv run --group docs mkdocs serve   # http://127.0.0.1:8000, live reload
uv run --group docs mkdocs build   # strict: broken links and bad docstrings fail

The READMEs are not copied into docs/: scripts/mkdocs_hooks.py adds them as pages and rewrites their relative links. A new package needs its README in PACKAGES there, a docs/reference/<pkg>.md page and entries in the nav of mkdocs.yml. Docstrings use the Google style.

The Sheets engine

packages/sheets/src/gsuite_sheets/engine/ is the engine incorporated from GSpreadManager. Its full history is kept in the archive/gspreadmanager tag (the original repository was deleted). It is hexagonal:

  • domain/: pure value objects (CellFormat, Color, ranges, validation, charts, schemas)
  • ports/sheets.py: ClientPort / SpreadsheetPort / WorksheetPort
  • application/: services that only talk to those ports
  • testing/in_memory.py: an in-memory implementation of the ports (a Sheets emulator)

gsuite_sheets/engine_adapter.py implements the ports on top of the google-suite client, so requests go through gsuite_core.execute like every other service. A1 parsing lives only in gsuite_sheets/a1.py; the engine delegates to it.

The engine tests run twice in CI: against the emulator, and through the real adapter over a fake googleapiclient service backed by the same emulator:

uv run pytest packages/sheets/tests                           # emulator
uv run pytest packages/sheets/tests --engine-backend=adapter  # through the adapter

A test that inspects the emulator itself (not port behavior) is marked @pytest.mark.memory_only.

The async engine works the same way: engine_async_adapter.py implements the async ports over gsuite_core.aio, and with --engine-backend=adapter the async engine tests go through it, against a fake Sheets/Drive REST server (engine/testing/rest_fake.py, an httpx.MockTransport) backed by the same emulator.

Getting Credentials

To test the library locally, you need Google OAuth credentials:

  1. Go to Google Cloud Console
  2. Create a new project (or select existing)
  3. Enable the APIs you need:
  4. Gmail API
  5. Google Calendar API
  6. Google Drive API
  7. Google Sheets API
  8. Go to APIs & Services > Credentials
  9. Click Create Credentials > OAuth client ID
  10. Select Desktop app as application type
  11. Download the JSON file
  12. Save it as credentials.json in the repo root

Important: Never commit credentials.json — it's in .gitignore.

Project Structure

google-suite/
├── packages/
│   ├── core/           # Shared auth, config, storage
│   │   ├── src/gsuite_core/
│   │   └── tests/
│   ├── gmail/          # Gmail client
│   ├── calendar/       # Calendar client
│   ├── drive/          # Drive client
│   └── sheets/         # Sheets client
├── api/                # FastAPI REST gateway
├── cli/                # Typer CLI
├── conftest.py         # Shared pytest fixtures
├── pyproject.toml      # Workspace config
└── README.md

Design Principles

  1. Package Independence: Each package can be installed and used independently
  2. Shared Auth: All packages use gsuite-core for authentication
  3. Pythonic API: Simple, intuitive interfaces inspired by libraries like gspread
  4. Type Hints: Full type annotations for IDE support
  5. Lazy Loading: API services are created on-demand

Making Changes

Adding a Feature

  1. Create a branch: git checkout -b feat/my-feature
  2. Make your changes
  3. Add tests for new functionality
  4. Update documentation (README, docstrings)
  5. Run tests and linting
  6. Commit with conventional message (see below)
  7. Open a pull request

Fixing a Bug

  1. Create a branch: git checkout -b fix/description
  2. Add a failing test that reproduces the bug
  3. Fix the bug
  4. Ensure all tests pass
  5. Commit and open PR

Commit Messages

We use Conventional Commits:

feat: add attachment support to Gmail send
fix: handle empty calendar response
docs: update Gmail README with search examples
test: add Calendar recurring event tests
refactor: simplify OAuth token refresh logic
chore: update dependencies

PRs are squash-merged and the PR title becomes the commit on main, so the title must follow this format (CI checks it). Releases are driven by it: fix: bumps the patch version, feat: the minor version, and feat!: or a BREAKING CHANGE: footer marks a breaking change.

Releases

Releases are automated with release-please. Every push to main updates a release PR that bumps the version in pyproject.toml and uv.lock and writes CHANGELOG.md. Merging that PR tags the release and publishes gsuite-sdk to PyPI through trusted publishing. Don't edit the version by hand: gsuite_core.__version__, the API and the CLI all read it from the installed package metadata.

Pull Request Guidelines

  • Keep PRs focused on a single change
  • Include tests for new functionality
  • Update relevant documentation
  • Ensure CI passes (tests + linting)
  • Request review from maintainers

Adding a New Package

To add a new Google API (e.g., Contacts):

  1. Create package structure:

    packages/contacts/
    ├── src/gsuite_contacts/
    │   ├── __init__.py
    │   ├── client.py
    │   └── py.typed
    ├── tests/
    │   └── test_contacts_client.py   # test module names must be unique repo-wide
    ├── pyproject.toml
    └── README.md
    

  2. Add dependency on gsuite-core in pyproject.toml

  3. Follow existing patterns from other packages
  4. Add router in api/src/gsuite_api/routes/
  5. Add commands in cli/src/gsuite_cli/
  6. Wire it into packaging and CI:
  7. where in [tool.setuptools.packages.find] and source in [tool.coverage.run] (root pyproject.toml)
  8. PACKAGES/SERVICES in scripts/ci_select_packages.py and a filter in .github/workflows/ci.yml
  9. TARGETS in scripts/mypy_ratchet.py, then uv run python scripts/mypy_ratchet.py --update
  10. Update main README with new package
  11. Add the docs pages (see Documentation)
  12. Cover it in the agent skill: skills/gsuite-sdk/SKILL.md and a references/ page

Code Style

  • Line length: 100 characters
  • Imports: Sorted with isort (via ruff)
  • Docstrings: Google style
  • Type hints: Required for public functions

Example:

def send_email(
    to: list[str],
    subject: str,
    body: str,
    html: bool = False,
) -> Message:
    """
    Send an email.

    Args:
        to: List of recipient email addresses
        subject: Email subject line
        body: Email body content
        html: Whether body is HTML (default: plain text)

    Returns:
        The sent Message object

    Raises:
        AuthenticationError: If not authenticated
        RateLimitError: If API rate limit exceeded
    """

Questions?

  • Open an issue for bugs or feature requests
  • Check existing issues before creating new ones
  • Be respectful and constructive

License

By contributing, you agree that your contributions will be licensed under the MIT License.

Maintainer setup

One-time repository settings that live on GitHub, not in the repo:

REPO=PabloAlaniz/google-suite

# Squash-only merges, with the PR title as the commit message
gh api -X PATCH repos/$REPO -F allow_merge_commit=false -F allow_rebase_merge=false \
  -F allow_squash_merge=true -f squash_merge_commit_title=PR_TITLE \
  -f squash_merge_commit_message=PR_BODY -F delete_branch_on_merge=true

# Private vulnerability reports (SECURITY.md points here) and Dependabot security PRs
gh api -X PUT repos/$REPO/private-vulnerability-reporting
gh api -X PUT repos/$REPO/automated-security-fixes

# Let release-please open PRs
gh api -X PUT repos/$REPO/actions/permissions/workflow \
  -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true

# Fine-grained PAT (this repo; contents + pull requests: write) so release PRs trigger CI
gh secret set RELEASE_PLEASE_TOKEN -R $REPO

# ClawHub API token (clawhub.ai > settings > API tokens) so releases publish the skill
gh secret set CLAWHUB_TOKEN -R $REPO --env clawhub

On PyPI, both projects publish through trusted publishing from this repo (workflow publish.yml, environment pypi): gsuite-sdk, and gsuite-mcp (added as a pending publisher before its first release at https://pypi.org/manage/account/publishing/).

# Protect main. Apply after ci-ok has run once on main, so the check exists.
gh api -X POST repos/$REPO/rulesets --input .github/rulesets/main.json

The ruleset requires a squash-merged PR with ci-ok, secret scanning, the dependency audit, CodeQL and the PR title check green. It has no bypass actors, so automation that pushed straight to main has to open PRs instead.