Initial commit (clean, ignores in place)

This commit is contained in:
2025-08-12 01:13:41 +01:00
commit c74790b014
26 changed files with 2331 additions and 0 deletions

8
tests/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
"""Test suite for the mileage logging tool.
The tests in this package exercise the core components of the mileage
logger. They use simple JSON fixtures to simulate a user's Google
Semantic Location History exports and assert that the itinerary
detection, distance resolution and Excel export modules behave as
expected.
"""

View File

@@ -0,0 +1,73 @@
# origin,destination,miles
Home,Lingwood Primary Academy,12.9
Home,Henderson Green Primary Academy,2.9
Home,Valley Primary Academy,2.1
Home,Heartsease Primary Academy,3.4
Home,Colman Junior School,3.1
Home,Colman Infant School,3.1
Home,Robert Kett Primary School,11.2
Home,Unity HQ,64.0
Lingwood Primary Academy,Home,12.9
Lingwood Primary Academy,Henderson Green Primary Academy,12.8
Lingwood Primary Academy,Valley Primary Academy,13.4
Lingwood Primary Academy,Heartsease Primary Academy,9.5999999999999996
Lingwood Primary Academy,Colman Junior School,11.300000000000001
Lingwood Primary Academy,Colman Infant School,11.1
Lingwood Primary Academy,Robert Kett Primary School,18.699999999999999
Lingwood Primary Academy,Unity HQ,63.100000000000001
Henderson Green Primary Academy,Home,2.8999999999999999
Henderson Green Primary Academy,Lingwood Primary Academy,12.800000000000001
Henderson Green Primary Academy,Valley Primary Academy,1.1000000000000001
Henderson Green Primary Academy,Heartsease Primary Academy,4.5
Henderson Green Primary Academy,Colman Junior School,1.8
Henderson Green Primary Academy,Colman Infant School,1.8
Henderson Green Primary Academy,Robert Kett Primary School,8
Henderson Green Primary Academy,Unity HQ,63.100000000000001
Valley Primary Academy,Home,2.1000000000000001
Valley Primary Academy,Lingwood Primary Academy,13.4
Valley Primary Academy,Henderson Green Primary Academy,1.1000000000000001
Valley Primary Academy,Heartsease Primary Academy,4.7000000000000002
Valley Primary Academy,Colman Junior School,2.2999999999999998
Valley Primary Academy,Colman Infant School,2.2999999999999998
Valley Primary Academy,Robert Kett Primary School,8
Valley Primary Academy,Unity HQ,64.700000000000003
Heartsease Primary Academy,Home,3.3999999999999999
Heartsease Primary Academy,Lingwood Primary Academy,9.5999999999999996
Heartsease Primary Academy,Henderson Green Primary Academy,4.5
Heartsease Primary Academy,Valley Primary Academy,4.7000000000000002
Heartsease Primary Academy,Colman Junior School,4.0999999999999996
Heartsease Primary Academy,Colman Infant School,3.8999999999999999
Heartsease Primary Academy,Robert Kett Primary School,14
Heartsease Primary Academy,Unity HQ,68.299999999999997
Colman Junior School,Home,3.1000000000000001
Colman Junior School,Lingwood Primary Academy,11.300000000000001
Colman Junior School,Henderson Green Primary Academy,1.8
Colman Junior School,Valley Primary Academy,2.2999999999999998
Colman Junior School,Heartsease Primary Academy,4.0999999999999996
Colman Junior School,Colman Infant School,0.20000000000000001
Colman Junior School,Robert Kett Primary School,8.4000000000000004
Colman Junior School,Unity HQ,62.600000000000001
Colman Infant School,Home,3.1000000000000001
Colman Infant School,Lingwood Primary Academy,11.1
Colman Infant School,Henderson Green Primary Academy,1.8
Colman Infant School,Valley Primary Academy,2.2999999999999998
Colman Infant School,Heartsease Primary Academy,3.8999999999999999
Colman Infant School,Colman Junior School,0.20000000000000001
Colman Infant School,Robert Kett Primary School,8.0999999999999996
Colman Infant School,Unity HQ,62.399999999999999
Robert Kett Primary School,Home,11.199999999999999
Robert Kett Primary School,Lingwood Primary Academy,18.699999999999999
Robert Kett Primary School,Henderson Green Primary Academy,8
Robert Kett Primary School,Valley Primary Academy,8
Robert Kett Primary School,Heartsease Primary Academy,14
Robert Kett Primary School,Colman Junior School,8.4000000000000004
Robert Kett Primary School,Colman Infant School,8.0999999999999996
Robert Kett Primary School,Unity HQ,57.299999999999997
Unity HQ,Home,64
Unity HQ,Lingwood Primary Academy,63.100000000000001
Unity HQ,Henderson Green Primary Academy,72.400000000000006
Unity HQ,Valley Primary Academy,64.700000000000003
Unity HQ,Heartsease Primary Academy,68.299999999999997
Unity HQ,Colman Junior School,62.600000000000001
Unity HQ,Colman Infant School,62.399999999999999
Unity HQ,Robert Kett Primary School,66.299999999999997
1 # origin destination miles
2 Home Lingwood Primary Academy 12.9
3 Home Henderson Green Primary Academy 2.9
4 Home Valley Primary Academy 2.1
5 Home Heartsease Primary Academy 3.4
6 Home Colman Junior School 3.1
7 Home Colman Infant School 3.1
8 Home Robert Kett Primary School 11.2
9 Home Unity HQ 64.0
10 Lingwood Primary Academy Home 12.9
11 Lingwood Primary Academy Henderson Green Primary Academy 12.8
12 Lingwood Primary Academy Valley Primary Academy 13.4
13 Lingwood Primary Academy Heartsease Primary Academy 9.5999999999999996
14 Lingwood Primary Academy Colman Junior School 11.300000000000001
15 Lingwood Primary Academy Colman Infant School 11.1
16 Lingwood Primary Academy Robert Kett Primary School 18.699999999999999
17 Lingwood Primary Academy Unity HQ 63.100000000000001
18 Henderson Green Primary Academy Home 2.8999999999999999
19 Henderson Green Primary Academy Lingwood Primary Academy 12.800000000000001
20 Henderson Green Primary Academy Valley Primary Academy 1.1000000000000001
21 Henderson Green Primary Academy Heartsease Primary Academy 4.5
22 Henderson Green Primary Academy Colman Junior School 1.8
23 Henderson Green Primary Academy Colman Infant School 1.8
24 Henderson Green Primary Academy Robert Kett Primary School 8
25 Henderson Green Primary Academy Unity HQ 63.100000000000001
26 Valley Primary Academy Home 2.1000000000000001
27 Valley Primary Academy Lingwood Primary Academy 13.4
28 Valley Primary Academy Henderson Green Primary Academy 1.1000000000000001
29 Valley Primary Academy Heartsease Primary Academy 4.7000000000000002
30 Valley Primary Academy Colman Junior School 2.2999999999999998
31 Valley Primary Academy Colman Infant School 2.2999999999999998
32 Valley Primary Academy Robert Kett Primary School 8
33 Valley Primary Academy Unity HQ 64.700000000000003
34 Heartsease Primary Academy Home 3.3999999999999999
35 Heartsease Primary Academy Lingwood Primary Academy 9.5999999999999996
36 Heartsease Primary Academy Henderson Green Primary Academy 4.5
37 Heartsease Primary Academy Valley Primary Academy 4.7000000000000002
38 Heartsease Primary Academy Colman Junior School 4.0999999999999996
39 Heartsease Primary Academy Colman Infant School 3.8999999999999999
40 Heartsease Primary Academy Robert Kett Primary School 14
41 Heartsease Primary Academy Unity HQ 68.299999999999997
42 Colman Junior School Home 3.1000000000000001
43 Colman Junior School Lingwood Primary Academy 11.300000000000001
44 Colman Junior School Henderson Green Primary Academy 1.8
45 Colman Junior School Valley Primary Academy 2.2999999999999998
46 Colman Junior School Heartsease Primary Academy 4.0999999999999996
47 Colman Junior School Colman Infant School 0.20000000000000001
48 Colman Junior School Robert Kett Primary School 8.4000000000000004
49 Colman Junior School Unity HQ 62.600000000000001
50 Colman Infant School Home 3.1000000000000001
51 Colman Infant School Lingwood Primary Academy 11.1
52 Colman Infant School Henderson Green Primary Academy 1.8
53 Colman Infant School Valley Primary Academy 2.2999999999999998
54 Colman Infant School Heartsease Primary Academy 3.8999999999999999
55 Colman Infant School Colman Junior School 0.20000000000000001
56 Colman Infant School Robert Kett Primary School 8.0999999999999996
57 Colman Infant School Unity HQ 62.399999999999999
58 Robert Kett Primary School Home 11.199999999999999
59 Robert Kett Primary School Lingwood Primary Academy 18.699999999999999
60 Robert Kett Primary School Henderson Green Primary Academy 8
61 Robert Kett Primary School Valley Primary Academy 8
62 Robert Kett Primary School Heartsease Primary Academy 14
63 Robert Kett Primary School Colman Junior School 8.4000000000000004
64 Robert Kett Primary School Colman Infant School 8.0999999999999996
65 Robert Kett Primary School Unity HQ 57.299999999999997
66 Unity HQ Home 64
67 Unity HQ Lingwood Primary Academy 63.100000000000001
68 Unity HQ Henderson Green Primary Academy 72.400000000000006
69 Unity HQ Valley Primary Academy 64.700000000000003
70 Unity HQ Heartsease Primary Academy 68.299999999999997
71 Unity HQ Colman Junior School 62.600000000000001
72 Unity HQ Colman Infant School 62.399999999999999
73 Unity HQ Robert Kett Primary School 66.299999999999997

View File

@@ -0,0 +1,107 @@
{
"timelineObjects": [
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1723090800000",
"endTimestampMs": "1723092600000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"endLocation": {
"latitudeE7": 524706000,
"longitudeE7": 13538000
},
"duration": {
"startTimestampMs": "1723092600000",
"endTimestampMs": "1723093200000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524706000,
"longitudeE7": 13538000,
"name": "Lingwood Primary Academy"
},
"duration": {
"startTimestampMs": "1723093200000",
"endTimestampMs": "1723111200000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524706000,
"longitudeE7": 13538000
},
"endLocation": {
"latitudeE7": 524634000,
"longitudeE7": 13627000
},
"duration": {
"startTimestampMs": "1723111200000",
"endTimestampMs": "1723112400000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524634000,
"longitudeE7": 13627000,
"name": "Heartsease Primary Academy"
},
"duration": {
"startTimestampMs": "1723112400000",
"endTimestampMs": "1723123200000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524634000,
"longitudeE7": 13627000
},
"endLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"duration": {
"startTimestampMs": "1723123200000",
"endTimestampMs": "1723125600000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1723125600000",
"endTimestampMs": "1723168800000"
}
}
}
]
}

View File

@@ -0,0 +1,137 @@
{
"timelineObjects": [
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1754980200000",
"endTimestampMs": "1754982000000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"endLocation": {
"latitudeE7": 524706000,
"longitudeE7": 13538000
},
"duration": {
"startTimestampMs": "1754982000000",
"endTimestampMs": "1754982900000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524706000,
"longitudeE7": 13538000,
"name": "Lingwood Primary Academy"
},
"duration": {
"startTimestampMs": "1754982900000",
"endTimestampMs": "1754989200000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524706000,
"longitudeE7": 13538000
},
"endLocation": {
"latitudeE7": 524710000,
"longitudeE7": 13590000
},
"duration": {
"startTimestampMs": "1754989200000",
"endTimestampMs": "1754990100000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524710000,
"longitudeE7": 13590000,
"name": "Valley Primary Academy"
},
"duration": {
"startTimestampMs": "1754990100000",
"endTimestampMs": "1754996400000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524710000,
"longitudeE7": 13590000
},
"endLocation": {
"latitudeE7": 524706000,
"longitudeE7": 13538000
},
"duration": {
"startTimestampMs": "1754996400000",
"endTimestampMs": "1754997300000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524706000,
"longitudeE7": 13538000,
"name": "Lingwood Primary Academy"
},
"duration": {
"startTimestampMs": "1754997300000",
"endTimestampMs": "1755003600000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524706000,
"longitudeE7": 13538000
},
"endLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"duration": {
"startTimestampMs": "1755003600000",
"endTimestampMs": "1755004500000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1755004500000",
"endTimestampMs": "1755014400000"
}
}
}
]
}

View File

@@ -0,0 +1,77 @@
{
"timelineObjects": [
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1754683200000",
"endTimestampMs": "1754692200000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"endLocation": {
"latitudeE7": 524634000,
"longitudeE7": 13627000
},
"duration": {
"startTimestampMs": "1754692200000",
"endTimestampMs": "1754695800000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524634000,
"longitudeE7": 13627000,
"name": "Heartsease Primary Academy"
},
"duration": {
"startTimestampMs": "1754695800000",
"endTimestampMs": "1754701200000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524634000,
"longitudeE7": 13627000
},
"endLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"duration": {
"startTimestampMs": "1754701200000",
"endTimestampMs": "1754703000000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1754703000000",
"endTimestampMs": "1754712000000"
}
}
}
]
}

View File

@@ -0,0 +1,137 @@
{
"timelineObjects": [
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1755154800000",
"endTimestampMs": "1755156600000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"endLocation": {
"latitudeE7": 524706000,
"longitudeE7": 13538000
},
"duration": {
"startTimestampMs": "1755156600000",
"endTimestampMs": "1755157500000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524706000,
"longitudeE7": 13538000,
"name": "Lingwood Primary Academy"
},
"duration": {
"startTimestampMs": "1755157500000",
"endTimestampMs": "1755162000000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524706000,
"longitudeE7": 13538000
},
"endLocation": {
"latitudeE7": 524670000,
"longitudeE7": 13550000
},
"duration": {
"startTimestampMs": "1755162000000",
"endTimestampMs": "1755162900000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524670000,
"longitudeE7": 13550000,
"name": "Nice Cafe"
},
"duration": {
"startTimestampMs": "1755162900000",
"endTimestampMs": "1755165600000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524670000,
"longitudeE7": 13550000
},
"endLocation": {
"latitudeE7": 524634000,
"longitudeE7": 13627000
},
"duration": {
"startTimestampMs": "1755165600000",
"endTimestampMs": "1755166500000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524634000,
"longitudeE7": 13627000,
"name": "Heartsease Primary Academy"
},
"duration": {
"startTimestampMs": "1755166500000",
"endTimestampMs": "1755172800000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524634000,
"longitudeE7": 13627000
},
"endLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"duration": {
"startTimestampMs": "1755172800000",
"endTimestampMs": "1755173700000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1755173700000",
"endTimestampMs": "1755190800000"
}
}
}
]
}

View File

@@ -0,0 +1,77 @@
{
"timelineObjects": [
{
"placeVisit": {
"location": {
"latitudeE7": 524610000,
"longitudeE7": 13500000,
"name": "Unity SP"
},
"duration": {
"startTimestampMs": "1755244800000",
"endTimestampMs": "1755248400000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524610000,
"longitudeE7": 13500000
},
"endLocation": {
"latitudeE7": 524634000,
"longitudeE7": 13627000
},
"duration": {
"startTimestampMs": "1755248400000",
"endTimestampMs": "1755250200000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524634000,
"longitudeE7": 13627000,
"name": "Heartsease Primary Academy"
},
"duration": {
"startTimestampMs": "1755250200000",
"endTimestampMs": "1755259200000"
}
}
},
{
"activitySegment": {
"startLocation": {
"latitudeE7": 524634000,
"longitudeE7": 13627000
},
"endLocation": {
"latitudeE7": 524649000,
"longitudeE7": 13460000
},
"duration": {
"startTimestampMs": "1755259200000",
"endTimestampMs": "1755261000000"
},
"activityType": "IN_PASSENGER_VEHICLE"
}
},
{
"placeVisit": {
"location": {
"latitudeE7": 524649000,
"longitudeE7": 13460000,
"name": "Home"
},
"duration": {
"startTimestampMs": "1755261000000",
"endTimestampMs": "1755277200000"
}
}
}
]
}

View File

@@ -0,0 +1,42 @@
import os
import unittest
from mileage_logger.logic.detect_itinerary import SiteConfig, haversine_distance
from mileage_logger.distance.resolve import DistanceResolver
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "sites.yml")
ROUTES_PATH = os.path.join(os.path.dirname(__file__), "data", "routes_golden.csv")
class TestDistanceResolver(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.site_config = SiteConfig.from_yaml(CONFIG_PATH)
cls.resolver = DistanceResolver(route_csv_path=ROUTES_PATH)
def test_route_lookup(self):
origin = "Home"
dest = "Lingwood Primary Academy"
origin_site = self.site_config.by_canonical[origin]
dest_site = self.site_config.by_canonical[dest]
dist = self.resolver.resolve(origin, dest, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon))
self.assertAlmostEqual(dist, 13.0, places=1)
# Second call should hit cache and return same
dist2 = self.resolver.resolve(origin, dest, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon))
self.assertEqual(dist2, dist)
def test_fallback_haversine(self):
# Choose a pair not in the route catalogue
origin = "Lingwood Primary Academy"
dest = "Unity SP"
origin_site = self.site_config.by_canonical[origin]
dest_site = self.site_config.by_canonical[dest]
dist = self.resolver.resolve(origin, dest, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon))
# Compute haversine expected
expected = haversine_distance(origin_site.lat, origin_site.lon, dest_site.lat, dest_site.lon)
self.assertAlmostEqual(dist, expected, places=1)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,91 @@
import os
import tempfile
import unittest
from openpyxl import load_workbook
from mileage_logger.ingest.semantic_reader import load_place_visits
from mileage_logger.logic.detect_itinerary import SiteConfig, detect_itinerary
from mileage_logger.distance.resolve import DistanceResolver
from mileage_logger.export.excel_writer import build_monthly_rows, write_monthly_workbook
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures", "semantic")
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "sites.yml")
ROUTES_PATH = os.path.join(os.path.dirname(__file__), "data", "routes_golden.csv")
class TestExcelWriter(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.site_config = SiteConfig.from_yaml(CONFIG_PATH)
def _build_workbook(self, fixture_file: str) -> str:
visits = load_place_visits(os.path.join(FIXTURES_DIR, fixture_file))
hops = detect_itinerary(visits, self.site_config)
resolver = DistanceResolver(route_csv_path=ROUTES_PATH)
rows_by_month = build_monthly_rows(hops, self.site_config, resolver)
# Write to a temporary file
fd, path = tempfile.mkstemp(suffix=".xlsx")
os.close(fd)
write_monthly_workbook(rows_by_month, path)
return path
def test_excel_layout_simple(self):
# Build workbook for the simple fixture
path = self._build_workbook("2025-08-08.one_day_simple.json")
try:
wb = load_workbook(path)
# Determine expected month from the first hop date
visits = load_place_visits(os.path.join(FIXTURES_DIR, "2025-08-08.one_day_simple.json"))
hops = detect_itinerary(visits, self.site_config)
if not hops:
self.fail("No hops detected for simple fixture")
expected_month = hops[0].date.strftime("%Y-%m")
self.assertIn(expected_month, wb.sheetnames)
ws = wb[expected_month]
rows = list(ws.iter_rows(values_only=True))
# number of hops + header
self.assertEqual(len(rows), len(hops) + 1)
header = rows[0]
expected_header = ("Date", "Purpose", "Miles", "Vehicle", "Job Role", "From", "To", "Notes")
self.assertEqual(header, expected_header)
# Validate each hop row
resolver = DistanceResolver(route_csv_path=ROUTES_PATH)
for i, hop in enumerate(hops, start=1):
row = rows[i]
origin_site = self.site_config.by_canonical[hop.origin]
dest_site = self.site_config.by_canonical[hop.destination]
dist = resolver.resolve(hop.origin, hop.destination, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon))
self.assertEqual(row[0], hop.date.isoformat())
expected_purpose = f"Travel from {origin_site.label} to {dest_site.label} {dist:.1f}mi"
self.assertEqual(row[1], expected_purpose)
self.assertAlmostEqual(float(row[2]), dist, places=1)
self.assertEqual(row[3], resolver.vehicle_label)
self.assertEqual(row[4], resolver.job_role)
self.assertEqual(row[5], origin_site.label)
self.assertEqual(row[6], dest_site.label)
# Notes may be returned as None when reading from Excel
self.assertIn(row[7] or "", ["", None])
finally:
os.unlink(path)
def test_cross_midnight_sheet_rows(self):
path = self._build_workbook("cross_midnight.json")
try:
wb = load_workbook(path)
# Should still be month 2025-08
self.assertIn("2025-08", wb.sheetnames)
ws = wb["2025-08"]
rows = list(ws.iter_rows(values_only=True))
# two hops -> 3 rows including header
self.assertEqual(len(rows), 3)
# Dates should span two days
dates = [r[0] for r in rows[1:]]
self.assertEqual(dates, ["2025-08-08", "2025-08-09"])
finally:
os.unlink(path)
if __name__ == '__main__':
unittest.main()

79
tests/test_itinerary.py Normal file
View File

@@ -0,0 +1,79 @@
import os
import unittest
from datetime import date
from mileage_logger.ingest.semantic_reader import load_place_visits
from mileage_logger.logic.detect_itinerary import SiteConfig, detect_itinerary
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures", "semantic")
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "sites.yml")
class TestItineraryDetection(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.site_config = SiteConfig.from_yaml(CONFIG_PATH)
def _load_visits(self, filename: str):
path = os.path.join(FIXTURES_DIR, filename)
return load_place_visits(path)
def test_one_day_simple(self):
visits = self._load_visits("2025-08-08.one_day_simple.json")
hops = detect_itinerary(visits, self.site_config)
canonical_pairs = [(h.origin, h.destination) for h in hops]
expected = [
("Home", "Lingwood Primary Academy"),
("Lingwood Primary Academy", "Heartsease Primary Academy"),
("Heartsease Primary Academy", "Home"),
]
self.assertEqual(canonical_pairs, expected)
def test_looping_multi_visit(self):
visits = self._load_visits("2025-08-12.looping.json")
hops = detect_itinerary(visits, self.site_config)
canonical_pairs = [(h.origin, h.destination) for h in hops]
expected = [
("Home", "Lingwood Primary Academy"),
("Lingwood Primary Academy", "Valley Primary Academy"),
("Valley Primary Academy", "Lingwood Primary Academy"),
("Lingwood Primary Academy", "Home"),
]
self.assertEqual(canonical_pairs, expected)
def test_detours_ignored(self):
visits = self._load_visits("day_with_detours.json")
hops = detect_itinerary(visits, self.site_config)
canonical_pairs = [(h.origin, h.destination) for h in hops]
expected = [
("Home", "Lingwood Primary Academy"),
("Lingwood Primary Academy", "Heartsease Primary Academy"),
("Heartsease Primary Academy", "Home"),
]
self.assertEqual(canonical_pairs, expected)
def test_no_home_start(self):
visits = self._load_visits("no_home_start.json")
hops = detect_itinerary(visits, self.site_config)
canonical_pairs = [(h.origin, h.destination) for h in hops]
expected = [
("Unity SP", "Heartsease Primary Academy"),
("Heartsease Primary Academy", "Home"),
]
self.assertEqual(canonical_pairs, expected)
def test_cross_midnight_dates(self):
visits = self._load_visits("cross_midnight.json")
hops = detect_itinerary(visits, self.site_config)
self.assertEqual(len(hops), 2)
# Ensure canonical names
self.assertEqual((hops[0].origin, hops[0].destination), ("Home", "Heartsease Primary Academy"))
self.assertEqual((hops[1].origin, hops[1].destination), ("Heartsease Primary Academy", "Home"))
# Check dates (local) across midnight
self.assertEqual(hops[0].date, date(2025, 8, 8))
self.assertEqual(hops[1].date, date(2025, 8, 9))
if __name__ == '__main__':
unittest.main()