Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,23 @@ max_conflict_display: 100

### CLI

The package provides a `timetracker` CLI with commands for both supported formats:
The package provides a `timetracker` CLI with unified commands for both supported formats:

```bash
# Show version
uv run timetracker --version

# Import a TimeCop CSV file and display entries
uv run timetracker timecop --config tests/timetracker.yml --input tests/example_timecop.csv

# Export the database back to TimeCop CSV
uv run timetracker timecop --config tests/timetracker.yml --output timecop_export.csv

uv run timetracker add --config tests/timetracker.yml --format timecop tests/example_timecop.csv

# Import a Simple Time Tracker CSV file and display entries
uv run timetracker stt --config tests/timetracker.yml --input tests/example_stt.csv
uv run timetracker add --config tests/timetracker.yml --format stt tests/example_stt.csv

# Export the database back to TimeCop CSV
uv run timetracker export --config tests/timetracker.yml --format timecop timecop_export.csv

# Export the database back to Simple Time Tracker CSV
uv run timetracker stt --config tests/timetracker.yml --output stt_export.csv
uv run timetracker export --config tests/timetracker.yml --format stt stt_export.csv
```

### Python API
Expand Down
33 changes: 0 additions & 33 deletions patch_serialise.py

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ warn_required_dynamic_aliases = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=timetracker_utils --cov-report=term-missing --cov-fail-under=80"
addopts = "--cov=timetracker_utils --cov-report=term-missing --cov-fail-under=95"

[tool.coverage.run]
source = ["timetracker_utils"]
Expand Down
74 changes: 63 additions & 11 deletions tests/test_base_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ def test_base_entry_parse_list_fields_string_fallback() -> None:
assert entry.categories == ["42"]


def test_base_entry_parse_list_fields_edge_case_empty_list_string() -> None:
"""Test parse_list_fields edge case where str(value).strip() == [] (line 177)."""
entry = BaseTimeEntry(
activity="Test",
start_time="2200-01-15T09:00:00.000Z",
end_time="2200-01-15T11:30:00.000Z",
categories=[],
)
assert entry.categories == []


def test_base_entry_parse_list_fields_non_string_empty() -> None:
"""Test that empty categories/tags defaults to empty list."""
entry = BaseTimeEntry(
Expand Down Expand Up @@ -199,26 +210,67 @@ def test_base_tracker_total_hours_by_activity_with_data() -> None:
tracker.total_hours_by_activity()


def test_base_entry_parse_datetime_naive_datetime_object() -> None:
"""Test parse_datetime with naive datetime object (tzinfo is None, line 120)."""
def test_base_entry_parse_datetime_naive_datetime_object_zone_none() -> None:
"""Test parse_datetime with naive datetime object when resolve_tz returns None."""
from datetime import datetime
from unittest.mock import patch

with patch("timetracker_utils.datetime_utils.resolve_tz", return_value=None):
entry = BaseTimeEntry.model_validate(
{
"activity": "Test",
"start_time": datetime(2200, 1, 15, 9, 0, 0), # naive datetime
"end_time": "2200-01-15T11:30:00.000Z",
},
context={"default_timezone": "ET"},
)
# Should still work, falling back to UTC
assert entry.start_time.tzinfo is not None

entry = BaseTimeEntry(
activity="Test",
start_time=datetime(2200, 1, 15, 9, 0, 0), # naive datetime
end_time="2200-01-15T11:30:00.000Z",
)
# Naive datetime should be assumed UTC

def test_base_entry_parse_datetime_naive_datetime_object_zone_not_none() -> None:
"""Test parse_datetime with naive datetime object when resolve_tz returns zone."""
from datetime import datetime, timedelta, timezone
from unittest.mock import patch

test_zone = timezone(timedelta(hours=-5))
with patch("timetracker_utils.datetime_utils.resolve_tz", return_value=test_zone):
entry = BaseTimeEntry.model_validate(
{
"activity": "Test",
"start_time": datetime(2200, 1, 15, 9, 0, 0), # naive datetime
"end_time": "2200-01-15T11:30:00.000Z",
},
context={"default_timezone": "ET"},
)
# Should convert to UTC
assert entry.start_time.tzinfo is not None


def test_base_entry_parse_datetime_naive_string_zone_none() -> None:
"""Test parse_datetime with naive string when resolve_tz returns None (line 152)."""
from unittest.mock import patch

with patch("timetracker_utils.datetime_utils.resolve_tz", return_value=None):
entry = BaseTimeEntry.model_validate(
{
"activity": "Test",
"start_time": "2200-01-15T09:00:00", # naive string
"end_time": "2200-01-15T11:30:00.000Z",
},
context={"default_timezone": "ET"},
)
# Should still work, falling back to UTC
assert entry.start_time.tzinfo is not None


def test_base_entry_hours_empty_string_returns_none() -> None:
"""Test hours validator with empty string (line 157)."""
def test_base_entry_hours_whitespace_string() -> None:
"""Test hours validator with whitespace-only string (line 187)."""
entry = BaseTimeEntry(
activity="Test",
start_time="2200-01-15T09:00:00.000Z",
end_time="2200-01-15T11:30:00.000Z",
hours="",
hours=" ",
)
# hours should be computed from end_time - start_time
assert entry.hours == 2.5
Expand Down
Loading
Loading