Python API Project Tutorial
Learn OpenPAUL by building a real project: a Python client that fetches gene information from the HGNC and NCBI Gene APIs using test-driven development.
What you'll build:
- Python client for HGNC REST API (
rest.genenames.org) - NCBI Gene API integration
- Full pytest test suite with mocks
- Test-driven development workflow with structured planning
Prerequisites:
- Python 3.10+
- uv package manager
- OpenCode (for running
/openpaul:commands) - Basic understanding of REST APIs
Step 1: Initialize the Project
# Create and initialize the project
mkdir gene-api-client
cd gene-api-client
uv init
# Add dependencies
uv add pytest pytest-mock requests
# Scaffold OpenPAUL
npx openpaul
npx openpaul creates two things:
.openpaul/state.json— project registry (name, version, timestamps).opencode/— OpenCode configuration and preset files
It does not create loop state files — those are created in the next step.
Step 2: Verify Plugin Installation
npx openpaul automatically added "plugin": ["openpaul"] to opencode.json. Restart OpenCode to load the plugin. The plugin registers slash commands on load, so /openpaul:help should autocomplete in the TUI.
Step 3: Initialize the Project in OpenCode
Open the project in OpenCode, then run:
/openpaul:init
This initializes the loop state and starts a brief conversation. OpenPAUL asks two questions:
"What's the core value this project delivers?"
"What are you building? (1-2 sentences)"
Example answers:
- Core value: "Developers can look up gene metadata by HGNC ID in a single function call"
- Description: "A Python client library for querying the HGNC and NCBI Gene REST APIs"
After the conversation, OpenPAUL creates:
.openpaul/model-config.json— model configuration.openpaul/state-phase-1.json— initial loop state.openpaul/phases/— directory for plans and summaries (created when you plan)
If you maintain a roadmap, create or edit .openpaul/ROADMAP.md now, or leave it for later.
Step 4: PLAN — Create First Plan
Use the TDD wizard in OpenCode. Enter /openpaul:plan with no flags and answer the step-by-step prompts for phase, plan ID, criteria, boundaries, and tasks.
OpenPAUL stores the plan at .openpaul/phases/1-01-PLAN.json and outputs a summary:
# 📋 Plan: 1-01
Type: execute | Wave: 1 | Tasks: 3
Structure: simple
## Criteria
- fetch_by_hgnc_id returns gene data dict with symbol, name, entrez_id
- network errors raise RequestException with context
- all tests pass with 100% coverage of client code
## Tasks
1. Write failing test for HGNC fetch: Test file exists and test fails
2. Implement HGNCClient: AC-1 and AC-2 satisfied
3. Add edge case tests: AC-3 satisfied — 100% coverage
Status: ✅ Plan created successfully
Location: .openpaul/phases/1-01-PLAN.json
Markdown: .openpaul/phases/1-01-PLAN.md
Next action: Run /openpaul:apply to execute the plan
Step 5: APPLY — Execute the Plan
/openpaul:apply
OpenPAUL displays each task in sequence with its action, verification step, and done criteria. Execute them one by one.
Task 1: Write Failing Test
# tests/test_hgnc_client.py
import pytest
import requests
from hgnc_client import HGNCClient
class TestHGNCClient:
def test_fetch_gene_by_id_success(self, mocker):
"""Test successful gene fetch by HGNC ID."""
mock_response = {
"responseHeader": {"status": 0},
"response": {
"docs": [{
"hgnc_id": "HGNC:5",
"symbol": "A1BG",
"name": "alpha-1-B glycoprotein",
"entrez_id": "1"
}]
}
}
mock_get = mocker.patch("requests.get")
mock_get.return_value = mocker.Mock(
status_code=200,
json=lambda: mock_response,
)
client = HGNCClient()
result = client.fetch_by_hgnc_id("HGNC:5")
assert result["symbol"] == "A1BG"
assert result["entrez_id"] == "1"
mock_get.assert_called_once_with(
"https://rest.genenames.org/fetch/hgnc_id/HGNC:5",
headers={"Accept": "application/json"},
timeout=30,
)
Verify: pytest tests/test_hgnc_client.py -v — test should fail (no implementation yet). ✓
Task 2: Implement HGNCClient
# src/hgnc_client.py
import requests
from typing import Optional, Dict, Any
class HGNCClient:
BASE_URL = "https://rest.genenames.org"
def __init__(self, timeout: int = 30):
self.timeout = timeout
self.headers = {"Accept": "application/json"}
def fetch_by_hgnc_id(self, hgnc_id: str) -> Optional[Dict[str, Any]]:
"""Fetch gene data by HGNC ID.
Args:
hgnc_id: HGNC identifier (e.g., "HGNC:5")
Returns:
Gene data dictionary or None if not found
Raises:
requests.RequestException: On network errors
"""
url = f"{self.BASE_URL}/fetch/hgnc_id/{hgnc_id}"
try:
response = requests.get(url, headers=self.headers, timeout=self.timeout)
response.raise_for_status()
docs = response.json().get("response", {}).get("docs", [])
return docs[0] if docs else None
except requests.RequestException as e:
raise requests.RequestException(f"Failed to fetch {hgnc_id}: {e}")
Verify: pytest tests/test_hgnc_client.py -v — AC-1 and AC-2 satisfied. ✓
Task 3: Edge Case Tests
# tests/test_hgnc_client.py (continued)
def test_fetch_gene_not_found(self, mocker):
"""Test gene not found returns None."""
mock_get = mocker.patch("requests.get")
mock_get.return_value = mocker.Mock(
status_code=200,
json=lambda: {"responseHeader": {"status": 0}, "response": {"docs": []}},
)
client = HGNCClient()
assert client.fetch_by_hgnc_id("HGNC:999999") is None
def test_fetch_network_error(self, mocker):
"""Test network error raises exception."""
mock_get = mocker.patch("requests.get")
mock_get.side_effect = requests.ConnectionError("Network error")
client = HGNCClient()
with pytest.raises(requests.RequestException):
client.fetch_by_hgnc_id("HGNC:5")
Verify: pytest tests/test_hgnc_client.py --cov=src -v — AC-3 satisfied. ✓
Step 6: UNIFY — Close the Loop
/openpaul:unify --status success \
--actuals '[
{"name": "Write failing test for HGNC fetch", "status": "completed"},
{"name": "Implement HGNCClient", "status": "completed"},
{"name": "Add edge case tests", "status": "completed"}
]'
OpenPAUL closes the loop, writes .openpaul/phases/1-01-SUMMARY.json, and advances state ready for the next plan. Output:
# 🔗 Loop Closed: 1-01
## Summary
Status: ✅ success
Tasks: ████████████ completed
### Tasks Completed
1. ✅ Write failing test for HGNC fetch
2. ✅ Implement HGNCClient
3. ✅ Add edge case tests
## Next Steps
✅ Loop successfully closed
Ready for: Next loop iteration
Run: /openpaul:plan to start planning the next phase
Step 7: Continue Development
Phase 2: NCBI Integration
Run /openpaul:plan and follow the TDD wizard prompts:
- Phase: 2
- Plan ID: 01
- Criteria: "given an entrez_id, fetch_ncbi_gene returns gene details from NCBI"; "given an HGNC ID, fetch_full_gene returns combined data from both APIs"
- Tasks (TDD order): write NCBI client tests, implement NCBIClient, add combined lookup
Execute and close:
/openpaul:apply
/openpaul:unify
Phase 3: CLI Tool
Run /openpaul:plan and follow the TDD wizard prompts:
- Phase: 3
- Plan ID: 01
- Criteria: "uv run gene-lookup HGNC:5 returns combined gene data"
- Tasks (TDD order): implement CLI entry point
Session Continuity
After a break:
/openpaul:resume
OpenPAUL loads your saved session, shows any file changes since you paused, and tells you exactly what to do next:
## 📋 Session Resume
Session ID: sess-...
Paused: 2024-01-15T14:30:00.000Z
Current Phase: 2 - APPLY
Work in Progress:
- Implementing NCBIClient.fetch_ncbi_gene
Next Steps:
- Resume task 2/3: Implement NCBIClient
📍 Loop: ✓ PLAN → ◉ APPLY → ○ UNIFY
Next Action: Run /openpaul:apply to execute the plan
Summary
You've learned:
- Scaffold —
npx openpaulcreates.openpaul/state.jsonand.opencode/config - Plugin installed —
npx openpaulautomatically adds"plugin": ["openpaul"]toopencode.json; restart OpenCode to load - Initialize —
/openpaul:initsets up loop state files - Plan —
/openpaul:planruns the TDD wizard and creates structured JSON plans with criteria and tasks - Apply —
/openpaul:applydisplays tasks for sequential execution with verification - Unify —
/openpaul:unifycloses the loop and writes a JSON summary - Resume —
/openpaul:resumerestores context after breaks
The loop ensures:
- Every unit of work is planned with acceptance criteria
- Execution stays bounded by task verification
- State persists across sessions as JSON
- Decisions and summaries are logged
- No orphan plans