# Review a device and software CSV with a local Python checker
Use this checker before sharing a device/software source log. Its JSON report names missing fields, repeated pairs and date or URL forms to review. Save the source below as check_records.py to run the checker locally.
Contents
# Supply the records
The CSV needs device, software, source_url and read_date headers. Dates use YYYY-MM-DD. The source_url field needs an HTTP or HTTPS URL with a hostname. Extra columns are allowed, but repeated header names are rejected.
Run python3 check_records.py records.csv from the folder holding the two files. Use - in place of records.csv to read from standard input. The checker uses Python’s standard library.
# Read the report
Read records for the row count and issues for the entries that need attention. Record numbering starts at two, after the header. The checker treats uppercase and lowercase names as the same when comparing pairs.
Exit status 0 means the checker found no row issues. Status 1 means the JSON report contains issues. Status 2 means a file-reading or header error prevented that report. A header-only CSV reports zero records with no row issues; check the count before treating it as a finished log.
# Publisher and limits
GolfCompat publishes the checker. It checks the supplied CSV fields without opening URLs or testing hardware and software compatibility. Read the source documents separately before making a compatibility decision.
Source record: Source-record CSV checker; publisher: GolfCompat; publication date: undated authored software, copyright 2026; source: complete original Python below. Keep its MIT license with copies.
# Complete Python source
#!/usr/bin/env python3
"""Check source-record CSV completeness without fetching URLs."""
import argparse
import csv
import datetime
import json
import sys
from urllib.parse import urlsplit
FIELDS = ("device", "software", "source_url", "read_date")
def check(stream):
reader = csv.DictReader(stream)
if not reader.fieldnames or any(f not in reader.fieldnames for f in FIELDS):
raise ValueError("Required columns: " + ", ".join(FIELDS))
if len(reader.fieldnames) != len(set(reader.fieldnames)):
raise ValueError("Duplicate column names")
issues = []
seen = {}
count = 0
for number, row in enumerate(reader, 2):
count += 1
errors = []
if None in row or any(row.get(f) is None for f in FIELDS):
errors.append("column-count")
values = {f: (row.get(f) or "").strip() for f in FIELDS}
for field in FIELDS:
if not values[field]:
errors.append("missing:" + field)
if values["source_url"]:
try:
url = urlsplit(values["source_url"])
valid = (url.scheme in ("http", "https") and url.hostname
and not url.username and not url.password
and not any(c.isspace() for c in values["source_url"]))
_ = url.port
except ValueError:
valid = False
if not valid:
errors.append("invalid:source_url")
if values["read_date"]:
date = values["read_date"]
try:
valid_date = (len(date) == 10 and datetime.date.fromisoformat(date).isoformat() == date)
except ValueError:
valid_date = False
if not valid_date:
errors.append("invalid:read_date")
pair = (values["device"].casefold(), values["software"].casefold())
if all(pair):
if pair in seen:
errors.append("duplicate-pair:record-" + str(seen[pair]))
else:
seen[pair] = number
if errors:
issues.append({"record": number, "issues": errors})
return {"records": count, "issues": issues}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("csv_file", help="CSV path, or - for standard input")
args = parser.parse_args()
try:
if args.csv_file == "-":
report = check(sys.stdin)
else:
with open(args.csv_file, encoding="utf-8-sig", newline="") as stream:
report = check(stream)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 1 if report["issues"] else 0
except (OSError, UnicodeError, ValueError, csv.Error) as error:
print(str(error), file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
# License
MIT License
Copyright (c) 2026 GolfCompat
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.