OpenViSTAI · Agents · Blockchain · Crypto
Menu

OPENVIST LEARN · FREE LESSONS

Project: build a bounded JSON reporting tool

Combine the earlier lessons into a useful local program with an explicit contract.

Lesson 21 of 22 · About 40 minutes · Beginner · English

By the end of this lesson

Run a complete CLI that validates a small task list before writing a summary.

Before you start

  • Python 3.12 or a compatible newer Python 3 interpreter; no external packages.
  • Complete lesson 20 in this route, or be comfortable with its examples.

Three words to know

Contract
The exact input and output behaviours a tool promises.
Bounded read
Reading at most a specified number of bytes plus one to detect excess.
Fail closed
Reject an invalid document rather than guessing a usable result.

1. Agree on the contract first

The input is one UTF-8 JSON array containing at most 1,000 objects. Each object must contain exactly title and status. title is nonblank text of at most 100 Python characters; status is exactly open or closed. Empty input lists are allowed. File size is at most 65,536 bytes, including whitespace. Duplicate object keys and non-standard NaN or Infinity constants are rejected. The code does not normalise or rewrite titles.

Read bounded bytes, parse JSON, validate every record, then emit the result or an error.
No report is written until the complete input has passed validation.

2. Save a known input

Create a new file named tasks.json in your practice folder using your editor. Paste the JSON below. The JSON block is file content, not a terminal command. The download links also provide this fixture and the source.

tasks.jsonJSON · save as tasks.json; do not run as Python
[{"title":"Read","status":"open"},{"title":"Test","status":"closed"},{"title":"Review","status":"open"}]

3. Read the complete implementation

Save this source as task_report.py in the same folder. summarise validates decoded values and returns data. read_records performs the bounded read and strict UTF-8 decode. main handles the CLI boundary. No summary is printed until every record has passed validation. The script only reads the named file and writes stdout or stderr; it does not modify the input.

task_report.pyPYTHON · save as task_report.py in your practice folder
# SPDX-License-Identifier: MIT
"""Learning project: bounded local task-status reporting, not a general JSON validator."""
import argparse
import json
import sys
from pathlib import Path

MAX_BYTES = 65536
MAX_RECORDS = 1000


def reject_constant(value):
    raise ValueError("non-standard JSON constant")


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("duplicate JSON key")
        result[key] = value
    return result


def summarise(records):
    if not isinstance(records, list) or len(records) > MAX_RECORDS:
        raise ValueError("expected at most 1000 records")
    counts = {"closed": 0, "open": 0}
    for record in records:
        if not isinstance(record, dict) or set(record) != {"title", "status"}:
            raise ValueError("expected title and status only")
        title, status = record["title"], record["status"]
        if not isinstance(title, str) or not title.strip() or len(title) > 100:
            raise ValueError("expected a nonblank title of at most 100 characters")
        if not isinstance(status, str) or status not in counts:
            raise ValueError("expected open or closed status")
        counts[status] += 1
    return {"records": len(records), "by_status": counts}


def read_records(path):
    with Path(path).open("rb") as handle:
        raw = handle.read(MAX_BYTES + 1)
    if len(raw) > MAX_BYTES:
        raise ValueError("input exceeds 65536 bytes")
    return json.loads(raw.decode("utf-8"), parse_constant=reject_constant,
                      object_pairs_hook=unique_object)


def main(argv=None):
    parser = argparse.ArgumentParser(description="Summarise a small local task list.")
    parser.add_argument("input", help="UTF-8 JSON file, at most 65536 bytes")
    args = parser.parse_args(argv)
    try:
        result = summarise(read_records(args.input))
    except (OSError, UnicodeError, ValueError, RecursionError):
        print("error: invalid or unreadable task file", file=sys.stderr)
        return 2
    print(json.dumps(result, sort_keys=True, allow_nan=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
What you should see · example output
{"by_status": {"closed": 1, "open": 2}, "records": 3}

4. Run and explain the result

Run the projectTERMINAL · Bash
python3 task_report.py tasks.json
What you should see · example output
{"by_status": {"closed": 1, "open": 2}, "records": 3}

Windows: use py -3 task_report.py tasks.json. You should see three records, one closed and two open. Output includes both status counters even when one is zero. A bad document produces one generic stderr message, exit status 2 and no JSON summary. A missing CLI argument is handled separately by argparse. The error message deliberately avoids echoing potentially private file contents.

YOUR TURN

A small check before you continue

Change one status to pending and run again. Should the two valid records still produce a partial summary?

Show a suggested answer

No. The entire document is rejected: stderr reports error: invalid or unreadable task file, the status is 2 and stdout is empty. Restore open or closed before rerunning.

Sources and scope

Documentation and examples reviewed 2026-10-18. Provider screens, package versions and output wording can differ. Examples show expected outcomes; they are not a claim that OpenViST connected to your server or verified your setup.