aiml
SearchMy learning
Menu

Lesson 5 of 36Module 3

Load developer documentation

Load a pinned local UTF-8 documentation snapshot into records that keep the source identity, version, URL and text together.

Prerequisites

The first two modules of Building Agentic RAG Applications; Python functions and JSON.

By the end, you can

  • Load a pinned local documentation snapshot into records that keep the source identity, version, URL and text together.
  • Preserve source metadata while normalizing decoded text.
  • Refuse changed bytes and unreadable source files before writing a corpus artifact.
Runtime
Python 3.11+; local files only
Dependencies
Python standard library; no model or third-party packages

Lab code, tests and fixtures (ZIP, 1.3 MB)Quiz check your understanding

Progress is saved in this browser.

Load a pinned local documentation snapshot into records that keep the source identity, version, URL and text together.

When you need this. A retriever can only search the text that reaches its corpus. Silently skipping a missing file or accepting changed bytes makes a baseline comparison hard to interpret. We will build the ingestion boundary before adding more retrieval behavior.

This lab uses the same original Python API summaries as the preceding baseline. They are supplied teaching text linked to official documentation, not downloaded Python documentation pages. The loader reads local files only. It does not crawl websites, parse HTML or call a model.

Prerequisites. Use Python and the supplied local fixtures. The vector retrieval course remains the prerequisite for embedding and vector-search internals; this module does not repeat them.

Pinned sourcemanifestLocal UTF-8summariesLoad + checkbytes and IDsDocuments +source metadataInvalid inputIngestionErrormetadatasource bytesrecordsfailurePinned sourcemanifestLocal UTF-8summariesLoad + checkbytes and IDsDocuments +source metadataInvalid inputIngestionErrormetadatainputrecordsfailure
The loader reads only manifest-listed local summaries. It checks the byte hash before decoding UTF-8 and returns document records. Invalid input stops the load; it does not produce a partial corpus.

Select the snapshot before reading files

A source manifest lists the files the application intends to load. Each entry in the supplied manifest carries a source ID, relative path, title, documentation version, URL and SHA-256 byte hash. The loader follows that list instead of recursively ingesting every file in a directory. An unlisted file stays outside the corpus.

A byte hash is a fingerprint for this snapshot. It detects that a file differs from the pinned input; it does not tell you whether the prose is accurate. Keep the source URL and the supplied fact-status field beside the text so later code can distinguish an original summary from a tested runtime result.

Refuse changed or unreadable source files

The loader reads a bounded number of bytes, compares the hash, decodes UTF-8 and rejects empty or NUL-containing text. It also rejects paths outside its allowed relative-path contract and symlinks. These checks support a local, nonconcurrent teaching directory. They are not a sandbox for a hostile process changing files during the read.

Changed bytes. Editing a summary without updating the pinned manifest raises IngestionError with hash_mismatch. Review the source change before creating a new snapshot. Do not repair the hash merely to make the command pass.

Unreadable input. A missing file raises IngestionError with unreadable_source. This strict loader stops the load and leaves the output-writing step unexecuted. An output from a previous successful run can still exist; use a fresh output path for each attempt.

Keep source metadata beside normalized text

The returned document contains text plus the source metadata. CRLF line endings are normalized to LF after the hash check. The hash continues to identify the original file bytes, not the normalized string. A later transform must preserve that distinction.

This document record is an ingestion artifact. It does not replace the earlier pipeline’s chunk IDs or eval targets. Connecting it to the existing splitter requires an explicit adapter that preserves source identity, URL and version; that adapter is outside this lab.

Run the local contract and inspect the result

Download the complete lab. The README, fixture metadata, implementation and tests travel together. No dependency installation or model server is required.

load_documents.pySource · pythonDownload
"""Load a pinned local documentation snapshot. No network or model calls."""
from __future__ import annotations
import argparse
import hashlib
import json
import stat
from pathlib import Path, PurePosixPath

MAX_BYTES = 64 * 1024

class IngestionError(ValueError):
    """A source cannot satisfy the ingestion contract."""

def load_source(root: Path, source: dict) -> dict:
    """Validate bytes before decoding; do not silently substitute changed text."""
    relative = source.get('path', '')
    if not isinstance(relative, str) or not relative or '\\' in relative:
        raise IngestionError('invalid_path')
    parts = PurePosixPath(relative)
    if parts.is_absolute() or '..' in parts.parts or parts.suffix != '.md':
        raise IngestionError('invalid_path')
    root = root.resolve()
    candidate = root / relative
    # Reject all symlinks, including links that currently resolve within the root.
    for parent in [candidate, *candidate.parents]:
        if parent == root:
            break
        if parent.is_symlink():
            raise IngestionError('symlink_source')
    path = candidate.resolve()
    if not path.is_relative_to(root):
        raise IngestionError('outside_root')
    try:
        if not stat.S_ISREG(path.stat().st_mode):
            raise IngestionError('nonregular_source')
        # Bounded read avoids loading a large file before enforcing the limit.
        with path.open('rb') as handle:
            raw = handle.read(MAX_BYTES + 1)
    except OSError as error:
        raise IngestionError('unreadable_source') from error
    if len(raw) > MAX_BYTES:
        raise IngestionError('size_limit')
    if hashlib.sha256(raw).hexdigest() != source.get('sha256'):
        raise IngestionError('hash_mismatch')
    try:
        body = raw.decode('utf-8')
    except UnicodeDecodeError as error:
        raise IngestionError('invalid_utf8') from error
    if not body.strip() or '\x00' in body:
        raise IngestionError('invalid_text')
    return {key: source[key] for key in ('source_id', 'path', 'title', 'sha256', 'version', 'url', 'fact_status') if key in source} | {'text': body.replace('\r\n', '\n')}

def read_manifest(path: Path) -> list[dict]:
    try:
        manifest = json.loads(path.read_text(encoding='utf-8'))
    except (OSError, UnicodeError, json.JSONDecodeError) as error:
        raise IngestionError('unreadable_manifest') from error
    if not isinstance(manifest, dict):
        raise IngestionError('invalid_manifest')
    sources = manifest.get('sources')
    if manifest.get('schema_version') != 1 or not isinstance(sources, list) or not sources:
        raise IngestionError('invalid_manifest')
    identities, paths = set(), set()
    for source in sources:
        if not isinstance(source, dict) or any(not isinstance(source.get(key), str) or not source[key] for key in ('source_id', 'path', 'title', 'sha256')):
            raise IngestionError('invalid_manifest')
        if any(key in source and (not isinstance(source[key], str) or not source[key]) for key in ('version', 'url', 'fact_status')):
            raise IngestionError('invalid_manifest')
        if source['source_id'] in identities or source['path'] in paths:
            raise IngestionError('duplicate_identity')
        identities.add(source['source_id']); paths.add(source['path'])
    return sources

def load_documents(manifest: Path) -> dict:
    sources = read_manifest(manifest)
    return {'schema_version': 1, 'execution_mode': 'offline-local-files',
            'documents': [load_source(manifest.parent, source) for source in sources]}

def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--manifest', type=Path, default=Path(__file__).parent / 'fixtures/source-manifest.json')
    parser.add_argument('--output', type=Path, required=True)
    args = parser.parse_args()
    result = load_documents(args.manifest)
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8')
    print(json.dumps({'execution_mode': result['execution_mode'], 'loaded': len(result['documents'])}, sort_keys=True))

if __name__ == '__main__':
    main()
Execute the local labTerminal · bash
python3 load_documents.py --output /tmp/loaded-documents.json
python3 -m unittest -v test_load_documents.py
Saved CLI outputRecorded output · jsonDownload
{
  "execution_mode": "offline-local-files",
  "loaded": 4
}

Inspect the loaded records and any rejected inputs in the saved result.

Inspect the complete saved result
Actual local ingestion resultRecorded output · jsonDownload
{
  "schema_version": 1,
  "execution_mode": "offline-local-files",
  "documents": [
    {
      "source_id": "path-write",
      "path": "docs/path-write.md",
      "title": "path-write",
      "sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
      "version": "3.12",
      "url": "https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.write_text",
      "fact_status": "original summary; primary docs checked",
      "text": "Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written."
    },
    {
      "source_id": "path-read",
      "path": "docs/path-read.md",
      "title": "path-read",
      "sha256": "dc097ca63486ace45279f412ea266bf754fcd2f0b1153f5d763ce905dafbc2c0",
      "version": "3.12",
      "url": "https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.read_text",
      "fact_status": "original summary; primary docs checked",
      "text": "Path.read_text(encoding=None, errors=None) opens a file, returns its text as a string and closes the file. Set encoding=\"utf-8\" when reading a UTF-8 file."
    },
    {
      "source_id": "json-text",
      "path": "docs/json-text.md",
      "title": "json-text",
      "sha256": "0f5efa9669a97f93d5e7e8fe91de80a0d954643947ac9619dba41d3904166288",
      "version": "3.12",
      "url": "https://docs.python.org/3.12/library/json.html#json.dumps",
      "fact_status": "original summary; primary docs checked",
      "text": "json.dumps(obj, ensure_ascii=True) returns a JSON string. Setting ensure_ascii=False leaves non-ASCII characters in that string. It does not write a file or select the encoding used by a file writer."
    },
    {
      "source_id": "path-read-3-13",
      "path": "docs/path-read-3-13.md",
      "title": "path-read-3-13",
      "sha256": "0dd5becb0c04458ad5f8bbf3fdef4f71034c584ef863bf748793bef7309b9f4f",
      "version": "3.13",
      "url": "https://docs.python.org/3.13/library/pathlib.html#pathlib.Path.read_text",
      "fact_status": "documented only; Python 3.13 not executed",
      "text": "In Python 3.13, Path.read_text accepts encoding, errors and newline. The newline parameter was added in Python 3.13. The method returns decoded file text and closes the file."
    }
  ]
}

Change one input and compare the failure

Copy the fixture directory to a scratch location. Change a source file while keeping its manifest unchanged, then run with --manifest pointing to the copied manifest and a new --output path. Compare the hash_mismatch failure with the successful run. Restore the bytes before continuing.

This CLI loads small local UTF-8 summaries. PDF extraction, HTML parsing, network retries and embedding throughput are unmeasured and unimplemented here.

A loaded file differs from its pinned hash. What should you do?

Choose one answer, then check your reasoning.

Read the explanation

File integrity, source authority and passage relevance are separate checks.

What’s next

Continue with Track Passage Sources.

References

Implementation APIs were checked on 23 October 2026 against official Python pathlib, hashlib and JSON documentation. The source summaries retain their version-specific official links and existing fact-status metadata. Python APIs use the standard library; no framework SDK is installed by this lab.

Written by Arjun Prabhulal.

Module 3

Document ingestion