Add ~ marker to exclude entries from CSV export
Prefix the project name or note column with ~ to mark an entry as count-but-don't-export. Marked entries are included in summary and status totals but omitted from all csv output (both --raw and aggregated, single-day and weekly). | 09:00 | 17:00 | 8:00 | ~Leave | | Day off | | 09:00 | 17:00 | 8:00 | Leave | | ~Day off | The ~ is stripped from whichever field carries it before any downstream processing, so project map resolution is unaffected. Implementation: - parse_table sets skip_csv=True on marked rows and strips the ~ - new filter_skip_csv() helper in parser.py - to_csv_entries() skips skip_csv rows - _cmd_csv calls filter_skip_csv() before aggregate_rows()
This commit is contained in:
parent
8b6f0b24e2
commit
de46399010
6 changed files with 209 additions and 25 deletions
|
|
@ -27,6 +27,7 @@ from .output import (
|
|||
from .parser import (
|
||||
aggregate_rows,
|
||||
filter_rows_by_date,
|
||||
filter_skip_csv,
|
||||
filter_week_sections,
|
||||
parse_document,
|
||||
resolve_overlaps,
|
||||
|
|
@ -436,7 +437,9 @@ def _cmd_csv(args: argparse.Namespace, config: dict) -> None:
|
|||
day_sections = [
|
||||
(
|
||||
_fmt(day),
|
||||
to_csv_entries(rows) if args.raw else aggregate_rows(rows),
|
||||
to_csv_entries(rows)
|
||||
if args.raw
|
||||
else aggregate_rows(filter_skip_csv(rows)),
|
||||
)
|
||||
for day, rows in day_sections_raw
|
||||
]
|
||||
|
|
@ -450,7 +453,9 @@ def _cmd_csv(args: argparse.Namespace, config: dict) -> None:
|
|||
rows = _resolve_rows(args, config, target_date)
|
||||
if not rows:
|
||||
print("Warning: no timesheet rows found in input.", file=sys.stderr)
|
||||
entries = to_csv_entries(rows) if args.raw else aggregate_rows(rows)
|
||||
entries = (
|
||||
to_csv_entries(rows) if args.raw else aggregate_rows(filter_skip_csv(rows))
|
||||
)
|
||||
if args.output:
|
||||
with open(args.output, "w", newline="", encoding="utf-8") as f:
|
||||
write_csv(entries, f, _fmt(target_date), project_map)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def to_csv_entries(rows: list[dict]) -> list[dict]:
|
|||
"quantity": row["duration_hours"],
|
||||
}
|
||||
for row in rows
|
||||
if row["duration_hours"] is not None
|
||||
if row["duration_hours"] is not None and not row.get("skip_csv")
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,19 @@ def parse_table(lines: list[str], has_duration_col: bool = True) -> list[dict]:
|
|||
note = strip_markdown_link(cells[4])
|
||||
duration = None
|
||||
|
||||
project_stripped = project.strip()
|
||||
if project_stripped.startswith("~"):
|
||||
skip_csv = True
|
||||
project = project_stripped[1:].strip()
|
||||
else:
|
||||
skip_csv = False
|
||||
project = project_stripped
|
||||
|
||||
note_stripped = note.strip()
|
||||
if note_stripped.startswith("~"):
|
||||
skip_csv = True
|
||||
note = note_stripped[1:].strip()
|
||||
|
||||
if start.lower() == "start":
|
||||
continue
|
||||
if not re.match(r"^\d+:\d{2}$", start):
|
||||
|
|
@ -112,17 +125,18 @@ def parse_table(lines: list[str], has_duration_col: bool = True) -> list[dict]:
|
|||
if not re.match(r"^\d+:\d{2}$", end):
|
||||
if duration is None:
|
||||
# No end and no duration — preserve as an open entry
|
||||
rows.append(
|
||||
{
|
||||
"start": start,
|
||||
"end": None,
|
||||
"duration_hours": None,
|
||||
"project": project,
|
||||
"story": story,
|
||||
"story_raw": story_raw,
|
||||
"note": note,
|
||||
}
|
||||
)
|
||||
row = {
|
||||
"start": start,
|
||||
"end": None,
|
||||
"duration_hours": None,
|
||||
"project": project,
|
||||
"story": story,
|
||||
"story_raw": story_raw,
|
||||
"note": note,
|
||||
}
|
||||
if skip_csv:
|
||||
row["skip_csv"] = True
|
||||
rows.append(row)
|
||||
continue
|
||||
|
||||
if duration is not None:
|
||||
|
|
@ -135,17 +149,18 @@ def parse_table(lines: list[str], has_duration_col: bool = True) -> list[dict]:
|
|||
except ValueError:
|
||||
continue
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"start": start,
|
||||
"end": end,
|
||||
"duration_hours": duration_hours,
|
||||
"project": project,
|
||||
"story": story,
|
||||
"story_raw": story_raw,
|
||||
"note": note,
|
||||
}
|
||||
)
|
||||
row = {
|
||||
"start": start,
|
||||
"end": end,
|
||||
"duration_hours": duration_hours,
|
||||
"project": project,
|
||||
"story": story,
|
||||
"story_raw": story_raw,
|
||||
"note": note,
|
||||
}
|
||||
if skip_csv:
|
||||
row["skip_csv"] = True
|
||||
rows.append(row)
|
||||
|
||||
return rows
|
||||
|
||||
|
|
@ -354,6 +369,11 @@ def resolve_overlaps(rows: list[dict]) -> list[dict]:
|
|||
return open_entries + closed
|
||||
|
||||
|
||||
def filter_skip_csv(rows: list[dict]) -> list[dict]:
|
||||
"""Return a copy of rows with any skip_csv-marked entries removed."""
|
||||
return [r for r in rows if not r.get("skip_csv")]
|
||||
|
||||
|
||||
def build_description(story: str, note: str) -> str:
|
||||
"""Combine story and note into a single description string."""
|
||||
parts = [p.strip() for p in [story, note] if p.strip()]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue