From fa2fcc29900763c4bfc03d9694121c14e08dfc2e Mon Sep 17 00:00:00 2001 From: Nicholas Karlson Date: Tue, 20 Jan 2026 14:30:53 -0800 Subject: [PATCH 1/3] Track D: add BYOD init (generate templates from contracts) --- byod_core_gl/README.md | 23 ++++ byod_core_gl/config.toml | 4 + byod_core_gl/tables/chart_of_accounts.csv | 1 + byod_core_gl/tables/gl_journal.csv | 1 + src/pystatsv1/cli.py | 43 ++++++++ src/pystatsv1/trackd/byod.py | 124 ++++++++++++++++++++++ src/pystatsv1/trackd/validate.py | 2 +- tests/test_trackd_byod_init_cli.py | 41 +++++++ 8 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 byod_core_gl/README.md create mode 100644 byod_core_gl/config.toml create mode 100644 byod_core_gl/tables/chart_of_accounts.csv create mode 100644 byod_core_gl/tables/gl_journal.csv create mode 100644 src/pystatsv1/trackd/byod.py create mode 100644 tests/test_trackd_byod_init_cli.py diff --git a/byod_core_gl/README.md b/byod_core_gl/README.md new file mode 100644 index 0000000..c4c8d7b --- /dev/null +++ b/byod_core_gl/README.md @@ -0,0 +1,23 @@ +# Track D — Bring Your Own Data (BYOD) + +This folder is a starter project for using your own accounting data with Track D. + +## What to edit + +- `tables/` contains **student-edited** CSVs. + Header-only templates are generated from the Track D contract. + +## What not to edit + +- `normalized/` is for **generated** outputs from future adapters. + +## Quickstart + +1) Fill in the required CSVs under `tables/`. +2) Validate your dataset: + + ```bash + pystatsv1 trackd validate --datadir tables --profile core_gl + ``` + +If validation fails, fix the missing files/columns and re-run. diff --git a/byod_core_gl/config.toml b/byod_core_gl/config.toml new file mode 100644 index 0000000..5a456b7 --- /dev/null +++ b/byod_core_gl/config.toml @@ -0,0 +1,4 @@ +# Track D BYOD project config +[trackd] +profile = "core_gl" +tables_dir = "tables" diff --git a/byod_core_gl/tables/chart_of_accounts.csv b/byod_core_gl/tables/chart_of_accounts.csv new file mode 100644 index 0000000..cd195e2 --- /dev/null +++ b/byod_core_gl/tables/chart_of_accounts.csv @@ -0,0 +1 @@ +account_id,account_name,account_type,normal_side diff --git a/byod_core_gl/tables/gl_journal.csv b/byod_core_gl/tables/gl_journal.csv new file mode 100644 index 0000000..676f0e5 --- /dev/null +++ b/byod_core_gl/tables/gl_journal.csv @@ -0,0 +1 @@ +txn_id,date,doc_id,description,account_id,debit,credit diff --git a/src/pystatsv1/cli.py b/src/pystatsv1/cli.py index 5820bea..47fbeb9 100644 --- a/src/pystatsv1/cli.py +++ b/src/pystatsv1/cli.py @@ -420,6 +420,27 @@ def cmd_trackd_validate(args: argparse.Namespace) -> int: return 0 + +def cmd_trackd_byod_init(args: argparse.Namespace) -> int: + # Keep CLI wiring lightweight: creation logic lives in pystatsv1.trackd.byod. + from pystatsv1.trackd import TrackDDataError + from pystatsv1.trackd.byod import init_byod_project + + try: + root = init_byod_project(args.dest, profile=args.profile, force=args.force) + except TrackDDataError as e: + print(str(e)) + return 1 + + print( + textwrap.dedent( + f"""\ + ✅ Track D BYOD project created at:\n + {root}\n + Next steps:\n 1) cd {root}\n 2) Fill in the required CSVs in tables/\n 3) pystatsv1 trackd validate --datadir tables --profile {args.profile}\n """ + ).rstrip() + ) + return 0 def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="pystatsv1", @@ -517,6 +538,28 @@ def build_parser() -> argparse.ArgumentParser: ) p_td_validate.set_defaults(func=cmd_trackd_validate) + p_td_byod = td_sub.add_parser("byod", help="Bring-your-own-data (BYOD) project helpers.") + byod_sub = p_td_byod.add_subparsers(dest="trackd_byod_cmd", required=True) + + p_byod_init = byod_sub.add_parser("init", help="Create a BYOD project folder with CSV header templates.") + p_byod_init.add_argument( + "--dest", + default="pystatsv1_trackd_byod", + help="Destination directory to create (default: pystatsv1_trackd_byod).", + ) + p_byod_init.add_argument( + "--profile", + default="core_gl", + choices=["core_gl", "ar", "full"], + help="Which profile to scaffold (default: core_gl).", + ) + p_byod_init.add_argument( + "--force", + action="store_true", + help="Allow writing into an existing non-empty directory.", + ) + p_byod_init.set_defaults(func=cmd_trackd_byod_init) + return p diff --git a/src/pystatsv1/trackd/byod.py b/src/pystatsv1/trackd/byod.py new file mode 100644 index 0000000..7faa007 --- /dev/null +++ b/src/pystatsv1/trackd/byod.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: MIT +"""Bring-your-own-data (BYOD) helpers for Track D. + +Phase 2 foundation: +- create a local BYOD project folder +- generate header-only CSV templates from the canonical Track D contracts + +Design: avoid shipping a second "header pack" contract. +We generate templates directly from :mod:`pystatsv1.trackd.schema` so the +single source of truth stays in one place. +""" + +from __future__ import annotations + +import csv +import textwrap +from pathlib import Path + +from ._errors import TrackDDataError +from ._types import PathLike +from .contracts import ALLOWED_PROFILES, schemas_for_profile + + +def init_byod_project(dest: PathLike, *, profile: str = "core_gl", force: bool = False) -> Path: + """Create a Track D BYOD project folder. + + The project layout is intentionally simple: + + - tables/ student-edited CSVs (header templates created here) + - raw/ optional dumps from source systems + - normalized/ adapter outputs (generated) + - notes/ assumptions, mapping notes, and QA + + Parameters + ---------- + dest: + Destination folder to create. + profile: + One of: core_gl, ar, full. + force: + Allow writing into an existing non-empty directory. + + Returns + ------- + Path + The created project root. + + Raises + ------ + TrackDDataError + If *dest* is non-empty and *force* is False, or if *profile* is invalid. + """ + + root = Path(dest).expanduser().resolve() + + if root.exists() and any(root.iterdir()) and not force: + raise TrackDDataError( + f"Refusing to write into non-empty directory: {root}\n" + "Use --force to overwrite into an existing directory." + ) + + p = (profile or "").strip().lower() + try: + schemas = schemas_for_profile(p) + except ValueError as e: + raise TrackDDataError( + f"Unknown profile: {profile}.\n" f"Use one of: {', '.join(ALLOWED_PROFILES)}" + ) from e + + # Create core folders + root.mkdir(parents=True, exist_ok=True) + (root / "tables").mkdir(parents=True, exist_ok=True) + (root / "raw").mkdir(parents=True, exist_ok=True) + (root / "normalized").mkdir(parents=True, exist_ok=True) + (root / "notes").mkdir(parents=True, exist_ok=True) + + # Write header-only CSV templates into tables/ + for schema in schemas: + out = root / "tables" / schema.name + with out.open("w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(list(schema.required_columns)) + + # Tiny config (write-only for now) + config = textwrap.dedent( + f"""\ + # Track D BYOD project config + [trackd] + profile = "{p}" + tables_dir = "tables" + """ + ).lstrip() + (root / "config.toml").write_text(config, encoding="utf-8") + + readme = textwrap.dedent( + f"""\ + # Track D — Bring Your Own Data (BYOD) + + This folder is a starter project for using your own accounting data with Track D. + + ## What to edit + + - `tables/` contains **student-edited** CSVs. + Header-only templates are generated from the Track D contract. + + ## What not to edit + + - `normalized/` is for **generated** outputs from future adapters. + + ## Quickstart + + 1) Fill in the required CSVs under `tables/`. + 2) Validate your dataset: + + ```bash + pystatsv1 trackd validate --datadir tables --profile {p} + ``` + + If validation fails, fix the missing files/columns and re-run. + """ + ).lstrip() + + (root / "README.md").write_text(readme, encoding="utf-8") + return root diff --git a/src/pystatsv1/trackd/validate.py b/src/pystatsv1/trackd/validate.py index 95cae4d..8f6a26a 100644 --- a/src/pystatsv1/trackd/validate.py +++ b/src/pystatsv1/trackd/validate.py @@ -55,7 +55,7 @@ def _format_report(report: dict[str, Any]) -> str: lines += [ "Fix: export the required CSV(s) and ensure the header names match the Track D contract.", - "Tip: compare your exported CSV headers against the workbook downloads.", + "Tip: generate a header-only template pack with: pystatsv1 trackd byod init --dest my_byod --profile ", ] return "\n".join(lines) diff --git a/tests/test_trackd_byod_init_cli.py b/tests/test_trackd_byod_init_cli.py new file mode 100644 index 0000000..e63880d --- /dev/null +++ b/tests/test_trackd_byod_init_cli.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from pathlib import Path + +from pystatsv1.cli import main + + +def test_trackd_byod_init_creates_structure_and_headers(tmp_path: Path, capsys) -> None: + dest = tmp_path / "byod" + + rc = main(["trackd", "byod", "init", "--dest", str(dest), "--profile", "core_gl"]) + out = capsys.readouterr().out + + assert rc == 0 + assert "BYOD project created" in out + + assert (dest / "tables").is_dir() + assert (dest / "raw").is_dir() + assert (dest / "normalized").is_dir() + assert (dest / "notes").is_dir() + assert (dest / "config.toml").exists() + assert (dest / "README.md").exists() + + coa = (dest / "tables" / "chart_of_accounts.csv").read_text(encoding="utf-8").splitlines()[0] + gl = (dest / "tables" / "gl_journal.csv").read_text(encoding="utf-8").splitlines()[0] + + assert coa == "account_id,account_name,account_type,normal_side" + assert gl == "txn_id,date,doc_id,description,account_id,debit,credit" + + +def test_trackd_byod_init_refuses_non_empty_dir_without_force(tmp_path: Path, capsys) -> None: + dest = tmp_path / "byod" + dest.mkdir() + (dest / "keep.txt").write_text("x", encoding="utf-8") + + rc = main(["trackd", "byod", "init", "--dest", str(dest), "--profile", "core_gl"]) + out = capsys.readouterr().out + + assert rc == 1 + assert "Refusing to write into non-empty directory" in out + assert "--force" in out From 03e2bc6f04a239c2eeef64431c637adbbe5ea931 Mon Sep 17 00:00:00 2001 From: Nicholas Karlson Date: Tue, 20 Jan 2026 14:34:36 -0800 Subject: [PATCH 2/3] Chore: ignore local BYOD output folder --- .gitignore | 3 +++ byod_core_gl/README.md | 23 ----------------------- byod_core_gl/config.toml | 4 ---- byod_core_gl/tables/chart_of_accounts.csv | 1 - byod_core_gl/tables/gl_journal.csv | 1 - 5 files changed, 3 insertions(+), 29 deletions(-) delete mode 100644 byod_core_gl/README.md delete mode 100644 byod_core_gl/config.toml delete mode 100644 byod_core_gl/tables/chart_of_accounts.csv delete mode 100644 byod_core_gl/tables/gl_journal.csv diff --git a/.gitignore b/.gitignore index 866eb66..00151f6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ outputs/ # Sphinx build outputs docs/build/ + +# Local BYOD projects (generated) +/byod_*/ diff --git a/byod_core_gl/README.md b/byod_core_gl/README.md deleted file mode 100644 index c4c8d7b..0000000 --- a/byod_core_gl/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Track D — Bring Your Own Data (BYOD) - -This folder is a starter project for using your own accounting data with Track D. - -## What to edit - -- `tables/` contains **student-edited** CSVs. - Header-only templates are generated from the Track D contract. - -## What not to edit - -- `normalized/` is for **generated** outputs from future adapters. - -## Quickstart - -1) Fill in the required CSVs under `tables/`. -2) Validate your dataset: - - ```bash - pystatsv1 trackd validate --datadir tables --profile core_gl - ``` - -If validation fails, fix the missing files/columns and re-run. diff --git a/byod_core_gl/config.toml b/byod_core_gl/config.toml deleted file mode 100644 index 5a456b7..0000000 --- a/byod_core_gl/config.toml +++ /dev/null @@ -1,4 +0,0 @@ -# Track D BYOD project config -[trackd] -profile = "core_gl" -tables_dir = "tables" diff --git a/byod_core_gl/tables/chart_of_accounts.csv b/byod_core_gl/tables/chart_of_accounts.csv deleted file mode 100644 index cd195e2..0000000 --- a/byod_core_gl/tables/chart_of_accounts.csv +++ /dev/null @@ -1 +0,0 @@ -account_id,account_name,account_type,normal_side diff --git a/byod_core_gl/tables/gl_journal.csv b/byod_core_gl/tables/gl_journal.csv deleted file mode 100644 index 676f0e5..0000000 --- a/byod_core_gl/tables/gl_journal.csv +++ /dev/null @@ -1 +0,0 @@ -txn_id,date,doc_id,description,account_id,debit,credit From db02d131025848886ee24e056cbf4bbd1baef8c0 Mon Sep 17 00:00:00 2001 From: Nicholas Karlson Date: Tue, 20 Jan 2026 14:40:48 -0800 Subject: [PATCH 3/3] Chore: ignore local BYOD output folder (pystatsv1_trackd_byod) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 00151f6..2c37510 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ docs/build/ # Local BYOD projects (generated) /byod_*/ + +# Local BYOD projects (generated) +/pystatsv1_trackd_byod/