11import datetime
22import os
3+ import zoneinfo
34from typing import Literal , TypedDict
45
56import duckdb
@@ -11,6 +12,7 @@ class Geofence(TypedDict):
1112 schedule_start : datetime .time | None
1213 schedule_end : datetime .time | None
1314 description : str
15+ timezone : zoneinfo .ZoneInfo
1416
1517
1618class GeofenceManager :
@@ -30,8 +32,34 @@ def refresh_geofences(self):
3032 schedule_stop::TIME as schedule_stop,
3133 description,
3234 ST_GeomFromGeoJSON(geojson) as geometry,
35+ timezone::VARCHAR as timezone,
3336 FROM read_csv('{ self .url } ')
34- )"""
37+ );
38+ -- convert a timestamp to a time in the given timezone
39+ CREATE OR REPLACE MACRO time_at(timestamp, timezone) AS (
40+ strftime((timestamp AT TIME ZONE timezone), '%H:%M:%S')::TIME
41+ );
42+ CREATE MACRO violations(lat, lon, timestamp) AS TABLE
43+ SELECT *, time_at(timestamp, timezone) AS time_at
44+ FROM geofences
45+ WHERE NOT ST_Contains(geometry, ST_Point(lon, lat))
46+ AND (
47+ status = 'on'
48+ OR
49+ (
50+ status = 'schedule'
51+ AND
52+ CASE
53+ WHEN schedule_start IS NULL THEN TRUE
54+ WHEN schedule_stop IS NULL THEN TRUE
55+ WHEN schedule_start <= schedule_stop THEN
56+ time_at(timestamp, timezone) >= schedule_start AND time_at(timestamp, timezone) <= schedule_stop
57+ ELSE
58+ time_at(timestamp, timezone) >= schedule_start OR time_at(timestamp, timezone) <= schedule_stop
59+ END
60+ )
61+ );
62+ """
3563 )
3664
3765 def get_geofences (self ) -> list [Geofence ]:
@@ -43,31 +71,24 @@ def violations(
4371 ) -> list [Geofence ]:
4472 """if the point is outside any geofence"""
4573 relation = self .conn .sql (
46- """
47- FROM geofences
48- WHERE NOT ST_Contains(geometry, ST_Point($lat, $lon))
49- AND (
50- status = 'on'
51- OR
52- (
53- status = 'schedule'
54- AND
55- CASE
56- WHEN schedule_start IS NULL THEN TRUE
57- WHEN schedule_stop IS NULL THEN TRUE
58- WHEN schedule_start <= schedule_stop THEN
59- schedule_start <= $localtime AND $localtime <= schedule_stop
60- ELSE
61- schedule_start <= $localtime OR $localtime <= schedule_stop
62- END
63- )
64- )
65- """ ,
66- params = {"lat" : lat , "lon" : lon , "localtime" : timestamp .time ()},
74+ "FROM violations($lat, $lon, $timestamp)" ,
75+ params = {"lat" : lat , "lon" : lon , "timestamp" : timestamp },
6776 )
6877 return self ._to_dicts (relation )
6978
70- def _to_dicts (self , relation : duckdb .DuckDBPyRelation ) -> list [Geofence ]:
71- cols = ["id" , "status" , "schedule_start" , "schedule_stop" , "description" ]
72- selection = relation .select (* cols )
73- return [Geofence (zip (cols , row )) for row in selection .fetchall ()]
79+ @staticmethod
80+ def _to_dicts (relation : duckdb .DuckDBPyRelation ) -> list [Geofence ]:
81+ cols = [
82+ "id" ,
83+ "status" ,
84+ "schedule_start" ,
85+ "schedule_stop" ,
86+ "description" ,
87+ "timezone" ,
88+ ]
89+ extra = [c for c in relation .columns if c not in cols ]
90+ cols = [* cols , * extra ]
91+ raw = [dict (zip (cols , row )) for row in relation .select (* cols ).fetchall ()]
92+ for row in raw :
93+ row ["timezone" ] = zoneinfo .ZoneInfo (row ["timezone" ])
94+ return [Geofence (** row ) for row in raw ]
0 commit comments