Thank you for your interest in contributing to mureo. This guide covers the development setup, coding standards, and PR workflow.
- Python 3.10 or later
- Git
- Node.js 20 or later (
node:testwas experimental before 20) — only to run the browser-asset tests (node --test tests/js/*.test.js). mureo ships no JavaScript dependencies, there is nopackage.jsonand no build step; Node is used purely as a test runner for the DOM-free logic inmureo/_data/web/reports_*.js. Skip it if you are not touching the configure UI — CI runs it either way.
git clone https://github.com/logly/mureo.git
cd mureo
# Install with dev tools
pip install -e ".[dev]"# Run the test suite
pytest tests/ -v
# Check types
mypy mureo/
# Check linting
ruff check mureo/pytest tests/ -vpytest --cov=mureo --cov-report=term-missingMinimum coverage: 80%. The CI pipeline will fail if coverage drops below this threshold (configured in pyproject.toml). Coverage must stay at or above 80%.
Tests are categorized with pytest markers:
# Unit tests only
pytest -m unit
# Integration tests only
pytest -m integrationThe 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.
- 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
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].contentFollow PEP 8 conventions. Formatting is enforced automatically.
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).
All data models must use frozen=True:
from dataclasses import dataclass
@dataclass(frozen=True)
class MyModel:
name: str
value: intFor fields containing mutable types (dict, list), use defensive copies in __post_init__ or convert to immutable types (tuple instead of list).
Never mutate existing objects. Create new instances instead:
# Good
new_entries = [*entries, new_entry]
# Bad
entries.append(new_entry)- 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/andmeta_ads/)
# 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
- Handle errors explicitly. Never silently swallow exceptions.
- API client methods should raise
RuntimeErrorwith user-facing messages. - Log technical details with the
loggingmodule, notprint().
Never commit credentials, API keys, or tokens. Use environment variables or ~/.mureo/credentials.json.
- Tests pass:
pytest tests/ -v - Coverage >= 80%:
pytest --cov=mureo --cov-report=term-missing - Types pass:
mypy mureo/ - Lint passes:
ruff check mureo/ - Formatted:
black --check mureo/ tests/ - Browser assets pass (only if you touched
mureo/_data/web/):node --test tests/js/*.test.js
- Title: concise summary under 70 characters
- Description: explain what and why, not just how
- Test plan: describe how the change was tested
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
When adding a new MCP tool:
- Client method: Add the async method to the appropriate Mixin in
mureo/google_ads/ormureo/meta_ads/. - Tool definition: Add a
Toolobject tomureo/mcp/tools_google_ads.pyortools_meta_ads.py. - Handler: Add a handler function and register it in the
_HANDLERSdict. - Tests: Add unit tests for both the client method and the handler.
- Documentation: Update
docs/mcp-server.mdwith the new tool.
- Add the command function to
mureo/cli/google_ads.pyormureo/cli/meta_ads.py. - Follow the existing pattern:
_require_creds()-> create client ->asyncio.run()->_output(). - Add tests.
- Update
docs/cli.md.
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
Open an issue on GitHub for questions, bug reports, or feature requests.