Skip to content

Latest commit

 

History

History
286 lines (204 loc) · 7.73 KB

File metadata and controls

286 lines (204 loc) · 7.73 KB

Contributing Guide

Thank you for your interest in contributing to mureo. This guide covers the development setup, coding standards, and PR workflow.

Development Setup

Prerequisites

  • Python 3.10 or later
  • Git
  • Node.js 20 or later (node:test was experimental before 20) — only to run the browser-asset tests (node --test tests/js/*.test.js). mureo ships no JavaScript dependencies, there is no package.json and no build step; Node is used purely as a test runner for the DOM-free logic in mureo/_data/web/reports_*.js. Skip it if you are not touching the configure UI — CI runs it either way.

Clone and Install

git clone https://github.com/logly/mureo.git
cd mureo

# Install with dev tools
pip install -e ".[dev]"

Verify Installation

# Run the test suite
pytest tests/ -v

# Check types
mypy mureo/

# Check linting
ruff check mureo/

Running Tests

Full Test Suite

pytest tests/ -v

With Coverage

pytest --cov=mureo --cov-report=term-missing

Minimum coverage: 80%. The CI pipeline will fail if coverage drops below this threshold (configured in pyproject.toml). Coverage must stay at or above 80%.

Test Markers

Tests are categorized with pytest markers:

# Unit tests only
pytest -m unit

# Integration tests only
pytest -m integration

Browser Assets

The configure UI in mureo/_data/web/ ships as plain <script>-loaded files — no bundler, no module system, no build step. Most of it is guarded by the tests/test_web_assets_*.py pins, which grep the shipped asset for the names, strings and selectors a feature depends on.

Grepping cannot catch an inverted condition, so the DOM-free parts of the Reports dashboard live in their own assets and are executed by Node's built-in test runner:

node --test tests/js/*.test.js
Asset Global What is in it
reports_logic.js MUREO_REPORTS_LOGIC KPI withholding, freshness aggregation, conflict routing
reports_format.js MUREO_REPORTS_FORMAT Flag labels and severities, param detail, numbers, period labels
reports_order.js MUREO_REPORTS_ORDER The Reports index card order and how it is persisted

No install step: no package.json, no dependencies, no lockfile. Each module publishes exactly one global for the browser and carries an inert module.exports tail so Node can require the exact bytes the browser is served. A new module needs three things: an entry in _STATIC_ALLOWLIST (mureo/web/handlers.py), a <script> tag ahead of dashboard.js in app.html, and a row in the table at the top of tests/js/browser_contract.test.js — which then asserts the same shipping contract for it as for every other module.

When you add DOM-free logic to the configure UI, put it in one of these and test it; rendering stays in dashboard.js and stays pinned statically.

Test Framework

  • pytest with pytest-asyncio (async tests auto-detected)
  • pytest-mock for mocking
  • All API calls must be mocked in tests -- no live API calls in CI

Writing Tests

Place tests in tests/ mirroring the source structure:

mureo/google_ads/client.py  →  tests/test_google_ads/test_client.py
mureo/context/strategy.py   →  tests/test_context/test_strategy.py

Example test:

import pytest
from mureo.context import parse_strategy, StrategyEntry

@pytest.mark.unit
def test_parse_strategy_persona():
    text = "# Strategy\n\n## Persona\nB2B SaaS buyers.\n"
    entries = parse_strategy(text)
    assert len(entries) == 1
    assert entries[0].context_type == "persona"
    assert "B2B" in entries[0].content

Coding Standards

PEP 8

Follow PEP 8 conventions. Formatting is enforced automatically.

Type Annotations

Required on all function signatures. mureo uses mypy --strict.

# Good
def get_campaign(doc: StateDocument, campaign_id: str) -> CampaignSnapshot | None:
    ...

# Bad (missing annotations)
def get_campaign(doc, campaign_id):
    ...

Use from __future__ import annotations at the top of every module for PEP 604 union syntax (X | Y).

Frozen Dataclasses

All data models must use frozen=True:

from dataclasses import dataclass

@dataclass(frozen=True)
class MyModel:
    name: str
    value: int

For fields containing mutable types (dict, list), use defensive copies in __post_init__ or convert to immutable types (tuple instead of list).

Immutability

Never mutate existing objects. Create new instances instead:

# Good
new_entries = [*entries, new_entry]

# Bad
entries.append(new_entry)

File Size

  • Target: 200-400 lines per file
  • Maximum: 800 lines
  • If a file grows beyond this, extract logic into separate modules (see the Mixin pattern used in google_ads/ and meta_ads/)

Formatting and Linting

# Format code
black mureo/ tests/

# Fix auto-fixable lint issues
ruff check --fix mureo/ tests/

# Type check
mypy mureo/

Configuration is in pyproject.toml:

  • black: line-length 88, target Python 3.10
  • ruff: select rules E, F, I, N, W, UP, B, A, SIM, TCH
  • mypy: strict mode

Error Handling

  • Handle errors explicitly. Never silently swallow exceptions.
  • API client methods should raise RuntimeError with user-facing messages.
  • Log technical details with the logging module, not print().

No Hardcoded Secrets

Never commit credentials, API keys, or tokens. Use environment variables or ~/.mureo/credentials.json.

Pull Request Guidelines

Before Submitting

  1. Tests pass: pytest tests/ -v
  2. Coverage >= 80%: pytest --cov=mureo --cov-report=term-missing
  3. Types pass: mypy mureo/
  4. Lint passes: ruff check mureo/
  5. Formatted: black --check mureo/ tests/
  6. Browser assets pass (only if you touched mureo/_data/web/): node --test tests/js/*.test.js

PR Structure

  • Title: concise summary under 70 characters
  • Description: explain what and why, not just how
  • Test plan: describe how the change was tested

Commit Messages

Follow Conventional Commits:

feat: add device performance analysis tool
fix: handle empty campaign list in state parser
refactor: extract keyword validation to shared helper
test: add coverage for Meta Ads rate limit retry
docs: update MCP server setup instructions

Adding a New Tool

When adding a new MCP tool:

  1. Client method: Add the async method to the appropriate Mixin in mureo/google_ads/ or mureo/meta_ads/.
  2. Tool definition: Add a Tool object to mureo/mcp/tools_google_ads.py or tools_meta_ads.py.
  3. Handler: Add a handler function and register it in the _HANDLERS dict.
  4. Tests: Add unit tests for both the client method and the handler.
  5. Documentation: Update docs/mcp-server.md with the new tool.

Adding a New CLI Command

  1. Add the command function to mureo/cli/google_ads.py or mureo/cli/meta_ads.py.
  2. Follow the existing pattern: _require_creds() -> create client -> asyncio.run() -> _output().
  3. Add tests.
  4. Update docs/cli.md.

Project Structure

mureo/
├── mureo/               # Source package
│   ├── __init__.py
│   ├── auth.py
│   ├── google_ads/
│   ├── meta_ads/
│   ├── analysis/
│   ├── context/
│   ├── cli/
│   └── mcp/
├── tests/               # Test suite (pytest)
│   └── js/              # Browser-asset tests (node --test, no deps)
├── docs/                # Documentation
├── pyproject.toml       # Project configuration
└── README.md

Questions?

Open an issue on GitHub for questions, bug reports, or feature requests.