DEV Community

Dakota Wu
Dakota Wu

Posted on

A Token-Budgeted Pipeline for Documenting and Testing a Legacy Repo

A legacy repo without tests or docs is a candidate for automation, not a rewrite. A token-aware pipeline can generate Markdown documentation and pytest skeletons in one pass, and a free managed server makes the math work for small projects. The script below walks a repository, estimates token cost per file, and calls an OpenAI-compatible endpoint with a hard budget — so the bill never surprises you.

MonkeyCode is an open-source project that provides a free server option and a 10M-token allowance for model access. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The pipeline described here works with any OpenAI-compatible endpoint, but the free allowance makes it practical to run against a whole repo.

Why Docs and Test Skeletons Are the Right Workload for a Free Tier

Documentation and test scaffolding are repetitive, low-risk, and bounded in size. Each file produces a small output, so the token cost per unit of value is low. That fits a free tier perfectly: the bottleneck is the allowance, not the quality of the result.

The alternative — writing docs and tests by hand — consumes hours that most teams do not have. The alternative to automation is not a better outcome; it is no outcome. A pipeline that generates a first draft is strictly better than an empty docs folder.

Step 1: Estimate Tokens Before You Spend Them

The first rule of a token budget is to measure before you spend. A rough estimate is len(text) / 4, which works well enough for English and code. Save this as a function:

def estimate_tokens(text: str) -> int:
    return max(1, len(text) // 4)
Enter fullscreen mode Exit fullscreen mode

This is not precise, but it is consistent. A consistent estimate is more useful than an exact one, because the goal is to compare files against a budget, not to bill a customer.

Step 2: Walk the Repo and Batch Files

The pipeline needs a list of Python files, sorted by size, with small files first. That ordering maximizes the number of files processed before the budget runs out.

import os
from pathlib import Path

SKIP_DIRS = {".git", "node_modules", "venv", "__pycache__", ".venv"}

def iter_python_files(root: Path):
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for fname in filenames:
            if fname.endswith(".py"):
                yield Path(dirpath) / fname

def files_by_size(root: Path):
    files = list(iter_python_files(root))
    return sorted(files, key=lambda p: p.stat().st_size)
Enter fullscreen mode Exit fullscreen mode

The sort matters. A 500-line module consumes more tokens than a 20-line helper, so processing small files first gives you more complete coverage when the budget is tight.

Step 3: Generate Docs and Tests With Retry Logic

The core loop calls the model once per file, with a prompt that asks for both documentation and a pytest skeleton. The response is written to two locations: docs/<relative_path>.md and tests/<relative_path>_test.py. Retry logic handles transient failures without burning the whole budget.

import os
import time
import requests
from pathlib import Path

API_URL = os.environ.get("MC_API_URL", "https://api.monkeycode.example/v1/chat/completions")
API_KEY = os.environ.get("MC_API_KEY", "")
MODEL = os.environ.get("MC_MODEL", "default")
MAX_TOKENS_PER_FILE = int(os.environ.get("MAX_TOKENS_PER_FILE", "1500"))
TOTAL_BUDGET = int(os.environ.get("TOTAL_BUDGET", "100000"))

def call_model(prompt: str, max_tokens: int) -> str:
    payload = {
        "model": MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": max_tokens,
        "temperature": 0.2,
    }
    headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
    for attempt in range(3):
        try:
            resp = requests.post(API_URL, json=payload, headers=headers, timeout=60)
            resp.raise_for_status()
            return resp.json()["choices"][0]["message"]["content"]
        except Exception as exc:
            print(f"  attempt {attempt + 1} failed: {exc}")
            time.sleep(2 ** attempt)
    raise RuntimeError(f"failed for prompt: {prompt[:80]}...")

def generate_for_file(path: Path, root: Path):
    code = path.read_text(encoding="utf-8", errors="replace")
    prompt = (
        "You are helping document a legacy Python repo. "
        "Return exactly two sections separated by '---':\n"
        "1. Markdown documentation for the module: purpose, public functions, usage example.\n"
        "2. A pytest skeleton with basic test functions for the main public functions.\n\n"
        f"```
{% endraw %}
python\n{code}\n
{% raw %}
```"
    )
    response = call_model(prompt, MAX_TOKENS_PER_FILE)
    doc_part, _, test_part = response.partition("---")
    rel = path.relative_to(root)
    doc_path = root / "docs" / rel.with_suffix(".md")
    test_path = root / "tests" / f"{rel.with_suffix('').as_posix().replace('/', '_')}_test.py"
    doc_path.parent.mkdir(parents=True, exist_ok=True)
    test_path.parent.mkdir(parents=True, exist_ok=True)
    doc_path.write_text(doc_part.strip() + "\n", encoding="utf-8")
    test_path.write_text(test_part.strip() + "\n", encoding="utf-8")
    print(f"  wrote {doc_path} and {test_path}")

def main(root: Path):
    for path in files_by_size(root):
        code = path.read_text(encoding="utf-8", errors="replace")
        est = estimate_tokens(code)
        if est + MAX_TOKENS_PER_FILE > TOTAL_BUDGET:
            print(f"  skip {path}: est {est} tokens exceeds remaining budget")
            continue
        print(f"  process {path}: est {est} tokens")
        generate_for_file(path, root)

if __name__ == "__main__":
    import sys
    root = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
    main(root)
Enter fullscreen mode Exit fullscreen mode

The prompt is deliberately strict about the output format. Without a separator, splitting documentation from tests is guesswork. The partition call handles a missing separator gracefully by leaving the test part empty, which the validation step catches later.

Step 4: Validate the Output Locally

Generated tests are a starting point, not a guarantee. Run pytest to see which ones pass and which ones are nonsense:

python -m pytest tests/ -q
Enter fullscreen mode Exit fullscreen mode

Expect failures. The value is in the skeleton: function names, import paths, and edge-case hints. Fixing a failing test is faster than writing one from scratch.

Who Should Not Use This Pipeline

Teams with strict data-residency rules should not route source code through any managed server, free or paid. Large codebases with hundreds of files will exhaust a 10M-token allowance quickly; the pipeline is designed for small to medium repos. Projects that require precise coverage metrics need human-written tests, not AI-generated skeletons.

Limitations

The 10M-token allowance and the free server option are availability claims that can change; verify current terms before committing a workflow to them. The token estimate is rough, so the actual consumption may differ. Generated docs and tests must be reviewed; they are not a substitute for a human maintainer. The script has no caching, so re-running it on the same repo spends tokens again. Add a file-based cache if you plan to iterate.

Run the pipeline on a single directory first. The output files and the token report will tell you whether the full repo is worth the spend — and whether the free tier is the right fit for your workflow.

MonkeyCode provides free models that can run this workflow.

Top comments (0)