# Camera source log checker
Version 0.1.0. A small Python tool for checking a camera research CSV before sharing it. It reads a local file and reports missing fields, duplicate model/question pairs, invalid date formats and source URLs containing embedded login details. It makes no network requests.
Contents
# Name the evidence state
Use documented for an answer tied to a source URL, observed for a result you recorded yourself, and unconfirmed for an open question. The checker requires an answer and date for completed evidence. A documented answer also needs a source URL. An unconfirmed row can keep those fields blank.
These checks concern the structure of your notes. They do not verify a manufacturer’s claim, inspect a camera, test compatibility or find every private detail in a file. Read the answers and source links yourself before sharing the CSV.
# Run it
Save the Python source below as camera_source_log.py. Use Python 3.9 or later; no additional packages are needed.
python3 camera_source_log.py research.csv
python3 camera_source_log.py research.csv --json
The CSV needs these six columns:
model,question,answer,source_url,checked_on,status
Example Camera,Which viewing device will I use?,,,,unconfirmed
Example Camera,Which document covers the mounting location?,,,,unconfirmed
The example model is a placeholder for your own research. Use the exact model label in a real log. Dates use YYYY-MM-DD; status values are lowercase. Exit code 0 means no checklist findings, 1 means findings were reported, and 2 means the file could not be read.
IP Camera Hub publishes documentation-led camera buying and setup references. This companion tool helps organize source notes; the CameraViewer app remains in development, with physical-camera tests pending.
# Source
#!/usr/bin/env python3
"""Check a camera research CSV locally; no network requests or compatibility tests."""
import argparse
import csv
import datetime
import json
import sys
from urllib.parse import urlsplit
COLUMNS = ("model", "question", "answer", "source_url", "checked_on", "status")
STATUSES = {"documented", "observed", "unconfirmed"}
def check(stream):
reader = csv.DictReader(stream)
missing = sorted(set(COLUMNS) - set(reader.fieldnames or []))
if missing:
return [{"row": 1, "field": "header", "issue": "missing columns: " + ", ".join(missing)}]
issues, seen = [], set()
for number, raw in enumerate(reader, 2):
row = {key: (raw.get(key) or "").strip() for key in COLUMNS}
def flag(field, issue):
issues.append({"row": number, "field": field, "issue": issue})
for field in ("model", "question"):
if not row[field]:
flag(field, "required value is blank")
identity = (row["model"].casefold(), row["question"].casefold())
if all(identity):
if identity in seen:
flag("question", "duplicate model/question pair")
seen.add(identity)
if row["status"] not in STATUSES:
flag("status", "use documented, observed or unconfirmed")
if row["checked_on"]:
try:
if datetime.date.fromisoformat(row["checked_on"]).isoformat() != row["checked_on"]:
raise ValueError
except ValueError:
flag("checked_on", "use an actual YYYY-MM-DD date")
elif row["status"] in {"documented", "observed"}:
flag("checked_on", "completed evidence needs a reading or observation date")
if row["source_url"]:
try:
address = urlsplit(row["source_url"])
valid = address.scheme in {"http", "https"} and bool(address.hostname)
if not valid:
flag("source_url", "use an absolute HTTP or HTTPS source URL")
if address.username is not None or address.password is not None:
flag("source_url", "remove embedded login details from the research log")
except ValueError:
flag("source_url", "URL could not be parsed")
if row["status"] == "documented" and not row["source_url"]:
flag("source_url", "documented answer needs a source URL")
if row["status"] in {"documented", "observed"} and not row["answer"]:
flag("answer", "completed evidence needs an answer or observation")
return issues
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("csv_file", help="UTF-8 CSV with the six documented columns")
parser.add_argument("--json", action="store_true", help="emit row/field findings as JSON")
args = parser.parse_args(argv)
try:
with open(args.csv_file, encoding="utf-8-sig", newline="") as stream:
issues = check(stream)
except (OSError, UnicodeError, csv.Error) as error:
print("Could not read CSV: " + str(error), file=sys.stderr)
return 2
if args.json:
print(json.dumps({"issues": issues, "issue_count": len(issues)}, indent=2))
elif issues:
for item in issues:
print(f"Row {item['row']} [{item['field']}]: {item['issue']}")
else:
print("No checklist issues found. Source truth and camera compatibility remain unchecked.")
return 1 if issues else 0
if __name__ == "__main__":
raise SystemExit(main())
# License
MIT License
Copyright (c) 2026 Amenti Labs LLC
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.