lucid.page Check the structure of a source-review log before saving JSON Lines
Text size
Read time2 min

# Check the structure of a source-review log before saving JSON Lines

Save this Python program as audit_source_log.py to check a source-review CSV before converting it to JSON Lines. The program reads your local file and makes no network requests.

Contents

# Prepare the columns

Use the headers topic, jurisdiction, source_title, source_url and checked_on. The review_note column is optional. Jurisdiction values may be blank; topic, source title, URL and check date need values.

Header names and cell values have surrounding whitespace removed. Duplicate headers and unsupported columns are rejected. Use an HTTP or HTTPS URL with a hostname, no credentials and no whitespace. The date must be a real calendar date written as YYYY-MM-DD.

# Run and read the output

Run python3 audit_source_log.py review.csv. On valid input, the program prints one JSON object per record and exits with status 0. It includes all six fields, with an empty review_note when that column is absent.

The utility reads and checks the complete file before writing records. Validation or file errors go to standard error with exit status 2. Empty files and duplicate topic, jurisdiction and URL combinations are rejected. Topic and jurisdiction comparisons ignore case; URL comparison uses the supplied text.

# Check the original sources

A successful run confirms the CSV passed the format checks above. The utility does not open source pages, verify a quoted requirement or determine whether a rule is current. Check the original page and its publication context when reviewing a requirement.

# Publisher

Haul Handbook publishes this source-log utility. Copy the complete source with its MIT license.

Source record: Source-review CSV utility; publisher: Haul Handbook; publication date: undated authored software, copyright 2026; source: complete original Python below.

# Complete source

python
#!/usr/bin/env python3
"""Check a current source-review CSV and emit JSON Lines without network access."""
import argparse
import csv
import datetime
import json
import re
import sys
from urllib.parse import urlsplit

FIELDS = ("topic", "jurisdiction", "source_title", "source_url", "checked_on", "review_note")
REQUIRED_HEADERS = set(FIELDS[:-1])
REQUIRED_VALUES = ("topic", "source_title", "source_url", "checked_on")


def read_records(path):
    with open(path, encoding="utf-8-sig", newline="") as source:
        reader = csv.DictReader(source, strict=True)
        headers = reader.fieldnames or []
        headers = [name.strip() for name in headers]
        if len(headers) != len(set(headers)):
            raise ValueError("duplicate column names")
        missing = REQUIRED_HEADERS - set(headers)
        extra = set(headers) - set(FIELDS)
        if missing or extra:
            raise ValueError("missing columns: " + ", ".join(sorted(missing)) +
                             "; unsupported columns: " + ", ".join(sorted(extra)))
        reader.fieldnames = headers
        return list(reader)


def validate(records):
    clean, errors, seen = [], [], set()
    for number, row in enumerate(records, 2):
        prefix = "record " + str(number) + ": "
        if None in row:
            errors.append(prefix + "more values than column names")
            continue
        values = {key: str(row.get(key) or "").strip() for key in FIELDS}
        missing = [key for key in REQUIRED_VALUES if not values[key]]
        if missing:
            errors.append(prefix + "empty fields: " + ", ".join(missing))
            continue
        url = values["source_url"]
        try:
            parsed = urlsplit(url)
            if (parsed.scheme not in ("http", "https") or not parsed.hostname or
                    parsed.username is not None or parsed.password is not None or
                    any(char.isspace() for char in url)):
                raise ValueError("invalid source URL")
            _ = parsed.port
        except ValueError:
            errors.append(prefix + "source_url needs an http(s) URL with a host and no credentials")
            continue
        stamp = values["checked_on"]
        try:
            if not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", stamp):
                raise ValueError("date format")
            datetime.date.fromisoformat(stamp)
        except ValueError:
            errors.append(prefix + "checked_on needs a calendar date in YYYY-MM-DD format")
            continue
        key = (values["topic"].casefold(), values["jurisdiction"].casefold(), url)
        if key in seen:
            errors.append(prefix + "duplicate topic, jurisdiction and source URL")
            continue
        seen.add(key)
        clean.append(values)
    if not records:
        errors.append("no source records")
    if errors:
        raise ValueError("\n".join(errors))
    return clean


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("csv_file", help="current source-review CSV")
    args = parser.parse_args(argv)
    try:
        records = validate(read_records(args.csv_file))
    except (ValueError, OSError, UnicodeError, csv.Error) as error:
        print(str(error), file=sys.stderr)
        return 2
    for record in records:
        print(json.dumps(record, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

# License

text
MIT License

Copyright (c) 2026 Haul Handbook

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
End