Skip to content

Commit c1e2f30

Browse files
discovery: default to wide schema, deprecate narrow
- discover_sensors() now defaults to schema="wide" (instant information_schema.columns lookup) instead of schema="narrow" - narrow schema path emits DeprecationWarning directing users to wide - Updated tests: split into TestDiscoverSensorsWide and TestDiscoverSensorsNarrow; added deprecation warning assertion - Bump version to 0.2.2
1 parent 42d0ccb commit c1e2f30

3 files changed

Lines changed: 126 additions & 66 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "slicks"
7-
version = "0.2.1"
7+
version = "0.2.2"
88
description = "The home baked data pipeline for Western Formula Racing"
99
readme = "README.md"
1010
authors = [

src/slicks/discovery.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
Sensor discovery module.
33
44
Scans the database for all unique sensor names within a time range.
5-
Uses adaptive chunking with parallel execution for narrow schema.
6-
For wide schema, uses instant information_schema.columns metadata lookup.
5+
For wide schema (default), uses an instant information_schema.columns metadata lookup.
6+
For narrow schema (legacy EAV), uses adaptive chunking with parallel execution.
77
"""
88

99
from __future__ import annotations
1010

1111
import threading
12+
import warnings
1213
from datetime import datetime, timedelta
1314
from typing import List, Optional
1415

@@ -26,24 +27,25 @@ def discover_sensors(
2627
chunk_size_days: int = 7,
2728
client=None,
2829
show_progress: bool = True,
29-
schema: str = "narrow",
30+
schema: str = "wide",
3031
) -> List[str]:
3132
"""
3233
Scan the database for ALL unique sensor names within the time range.
3334
34-
For ``schema="wide"``, uses an instant ``information_schema.columns`` metadata
35-
lookup (no data scan, no adaptive bisection, ignores time range and chunk params).
35+
For ``schema="wide"`` (default), uses an instant ``information_schema.columns``
36+
metadata lookup (no data scan, no adaptive bisection, ignores time range and
37+
chunk params).
3638
37-
For ``schema="narrow"`` (default), uses adaptive chunking with parallel execution
38-
to handle server resource limits efficiently.
39+
For ``schema="narrow"`` (legacy EAV, deprecated), uses adaptive chunking with
40+
parallel execution to handle server resource limits efficiently.
3941
4042
Args:
4143
start_time: Start of scan range (narrow schema only).
4244
end_time: End of scan range (narrow schema only).
4345
chunk_size_days: Days per chunk (default 7, narrow schema only).
4446
client: Ignored (kept for backward compatibility).
4547
show_progress: Show progress bar (default True, narrow schema only).
46-
schema: "narrow" (legacy EAV) or "wide" (one field per signal).
48+
schema: "wide" (default, one column per signal) or "narrow" (legacy EAV, deprecated).
4749
4850
Returns:
4951
Sorted list of unique sensor name strings.
@@ -68,7 +70,13 @@ def discover_sensors(
6870
if v.as_py() is not None and v.as_py() not in NON_SIGNAL_COLS
6971
)
7072

71-
# --- narrow (legacy EAV) path ---
73+
# --- narrow (legacy EAV) path — deprecated, wide schema is now standard ---
74+
warnings.warn(
75+
"schema='narrow' is deprecated and will be removed in a future release. "
76+
"WFR has moved to wide schema — use schema='wide' (now the default).",
77+
DeprecationWarning,
78+
stacklevel=2,
79+
)
7280

7381
def _make_client():
7482
return get_influx_client()

tests/test_discovery.py

Lines changed: 108 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
- Arrow-direct extraction (no pandas)
77
- Deduplication and sorting
88
- Error classification and propagation
9+
- Wide schema (default) vs narrow schema (legacy, deprecated)
910
"""
1011

1112
import unittest
13+
import warnings
1214
from datetime import datetime
1315
from unittest.mock import MagicMock, patch
1416
import sys
@@ -19,31 +21,34 @@
1921
from slicks.discovery import discover_sensors
2022

2123

22-
class TestDiscoverSensorsMocked(unittest.TestCase):
23-
"""Tests for discover_sensors with mocked InfluxDB."""
24+
def _make_wide_mock_config(mock_config):
25+
"""Set config attributes expected by the wide schema path."""
26+
mock_config.INFLUX_SCHEMA = "iox"
27+
mock_config.INFLUX_TABLE = "WFR26"
28+
mock_config.INFLUX_DB = "WFR26"
29+
30+
31+
class TestDiscoverSensorsWide(unittest.TestCase):
32+
"""Tests for discover_sensors with wide schema (default)."""
2433

2534
@patch('slicks.discovery.get_influx_client')
2635
@patch('slicks.discovery.config')
27-
def test_returns_sorted_unique_sensors(self, mock_config, mock_client_class):
28-
"""Should return sorted deduplicated sensor list."""
29-
mock_config.INFLUX_URL = "http://localhost:8086"
30-
mock_config.INFLUX_TOKEN = "test-token"
31-
mock_config.INFLUX_DB = "test-db"
36+
def test_returns_sorted_sensors(self, mock_config, mock_client_class):
37+
"""Should return sorted sensor list from column metadata."""
38+
_make_wide_mock_config(mock_config)
3239

33-
mock_col_values = [MagicMock(), MagicMock(), MagicMock()]
40+
mock_col_values = [MagicMock(), MagicMock()]
3441
mock_col_values[0].as_py.return_value = "SensorB"
3542
mock_col_values[1].as_py.return_value = "SensorA"
36-
mock_col_values[2].as_py.return_value = "SensorB" # duplicate
3743

3844
mock_table = MagicMock()
39-
mock_table.num_rows = 3
45+
mock_table.num_rows = 2
4046
mock_col = MagicMock()
4147
mock_col.__iter__ = MagicMock(return_value=iter(mock_col_values))
4248
mock_table.column.return_value = mock_col
4349

4450
mock_client = MagicMock()
4551
mock_client.query.return_value = mock_table
46-
mock_client.close = MagicMock()
4752
mock_client_class.return_value = mock_client
4853

4954
result = discover_sensors(
@@ -57,16 +62,13 @@ def test_returns_sorted_unique_sensors(self, mock_config, mock_client_class):
5762
@patch('slicks.discovery.get_influx_client')
5863
@patch('slicks.discovery.config')
5964
def test_returns_empty_list_when_no_data(self, mock_config, mock_client_class):
60-
"""Should return empty list when no sensors found."""
61-
mock_config.INFLUX_URL = "http://localhost:8086"
62-
mock_config.INFLUX_TOKEN = "test-token"
63-
mock_config.INFLUX_DB = "test-db"
65+
"""Should return empty list when no columns found."""
66+
_make_wide_mock_config(mock_config)
6467

6568
mock_table = MagicMock()
6669
mock_table.num_rows = 0
6770
mock_client = MagicMock()
6871
mock_client.query.return_value = mock_table
69-
mock_client.close = MagicMock()
7072
mock_client_class.return_value = mock_client
7173

7274
result = discover_sensors(
@@ -77,33 +79,11 @@ def test_returns_empty_list_when_no_data(self, mock_config, mock_client_class):
7779

7880
self.assertEqual(result, [])
7981

80-
@patch('slicks.discovery.get_influx_client')
81-
@patch('slicks.discovery.config')
82-
def test_raises_on_permanent_error(self, mock_config, mock_client_class):
83-
"""Should raise RuntimeError on auth failure."""
84-
mock_config.INFLUX_URL = "http://localhost:8086"
85-
mock_config.INFLUX_TOKEN = "bad-token"
86-
mock_config.INFLUX_DB = "test-db"
87-
88-
mock_client = MagicMock()
89-
mock_client.query.side_effect = Exception("Unauthorized: invalid token")
90-
mock_client.close = MagicMock()
91-
mock_client_class.return_value = mock_client
92-
93-
with self.assertRaises(RuntimeError):
94-
discover_sensors(
95-
start_time=datetime(2025, 1, 1),
96-
end_time=datetime(2025, 1, 2),
97-
show_progress=False,
98-
)
99-
10082
@patch('slicks.discovery.get_influx_client')
10183
@patch('slicks.discovery.config')
10284
def test_filters_none_values(self, mock_config, mock_client_class):
103-
"""Should skip None values in the signal name column."""
104-
mock_config.INFLUX_URL = "http://localhost:8086"
105-
mock_config.INFLUX_TOKEN = "test-token"
106-
mock_config.INFLUX_DB = "test-db"
85+
"""Should skip None values in the column_name column."""
86+
_make_wide_mock_config(mock_config)
10787

10888
mock_col_values = [MagicMock(), MagicMock()]
10989
mock_col_values[0].as_py.return_value = "SensorA"
@@ -117,7 +97,6 @@ def test_filters_none_values(self, mock_config, mock_client_class):
11797

11898
mock_client = MagicMock()
11999
mock_client.query.return_value = mock_table
120-
mock_client.close = MagicMock()
121100
mock_client_class.return_value = mock_client
122101

123102
result = discover_sensors(
@@ -130,53 +109,126 @@ def test_filters_none_values(self, mock_config, mock_client_class):
130109

131110
@patch('slicks.discovery.get_influx_client')
132111
@patch('slicks.discovery.config')
133-
def test_default_chunk_size_is_7_days(self, mock_config, mock_client_class):
134-
"""Default chunk_size_days=7 should produce 2 chunks for 10-day range."""
135-
mock_config.INFLUX_URL = "http://localhost:8086"
136-
mock_config.INFLUX_TOKEN = "test-token"
137-
mock_config.INFLUX_DB = "test-db"
112+
def test_single_query_regardless_of_time_range(self, mock_config, mock_client_class):
113+
"""Wide schema should issue exactly one metadata query, ignoring time range."""
114+
_make_wide_mock_config(mock_config)
138115

139116
mock_table = MagicMock()
140117
mock_table.num_rows = 0
141118
mock_client = MagicMock()
142119
mock_client.query.return_value = mock_table
143-
mock_client.close = MagicMock()
144120
mock_client_class.return_value = mock_client
145121

146122
discover_sensors(
147123
start_time=datetime(2025, 1, 1),
148-
end_time=datetime(2025, 1, 11),
124+
end_time=datetime(2025, 12, 31),
149125
show_progress=False,
150126
)
151127

152-
# 10-day range with 7-day chunks = 2 chunks, each triggers at least 1 query
153-
self.assertGreaterEqual(mock_client.query.call_count, 2)
128+
self.assertEqual(mock_client.query.call_count, 1)
154129

155130
@patch('slicks.discovery.get_influx_client')
156131
@patch('slicks.discovery.config')
157132
def test_backward_compatible_client_param(self, mock_config, mock_client_class):
158133
"""Passing client= should not raise (backward compat), even though unused."""
159-
mock_config.INFLUX_URL = "http://localhost:8086"
160-
mock_config.INFLUX_TOKEN = "test-token"
161-
mock_config.INFLUX_DB = "test-db"
134+
_make_wide_mock_config(mock_config)
162135

163136
mock_table = MagicMock()
164137
mock_table.num_rows = 0
165138
mock_client = MagicMock()
166139
mock_client.query.return_value = mock_table
167-
mock_client.close = MagicMock()
168140
mock_client_class.return_value = mock_client
169141

170-
old_client = MagicMock()
171142
result = discover_sensors(
172143
start_time=datetime(2025, 1, 1),
173144
end_time=datetime(2025, 1, 2),
174-
client=old_client,
145+
client=MagicMock(),
175146
show_progress=False,
176147
)
177148

178149
self.assertEqual(result, [])
179150

180151

152+
class TestDiscoverSensorsNarrow(unittest.TestCase):
153+
"""Tests for discover_sensors with narrow schema (legacy EAV, deprecated)."""
154+
155+
def _make_narrow_config(self, mock_config):
156+
mock_config.INFLUX_URL = "http://localhost:8086"
157+
mock_config.INFLUX_TOKEN = "test-token"
158+
mock_config.INFLUX_SCHEMA = "iox"
159+
mock_config.INFLUX_TABLE = "WFR26"
160+
mock_config.INFLUX_DB = "WFR26"
161+
162+
@patch('slicks.discovery.get_influx_client')
163+
@patch('slicks.discovery.config')
164+
def test_emits_deprecation_warning(self, mock_config, mock_client_class):
165+
"""schema='narrow' should emit a DeprecationWarning."""
166+
self._make_narrow_config(mock_config)
167+
168+
mock_table = MagicMock()
169+
mock_table.num_rows = 0
170+
mock_client = MagicMock()
171+
mock_client.query.return_value = mock_table
172+
mock_client_class.return_value = mock_client
173+
174+
with warnings.catch_warnings(record=True) as caught:
175+
warnings.simplefilter("always")
176+
discover_sensors(
177+
start_time=datetime(2025, 1, 1),
178+
end_time=datetime(2025, 1, 2),
179+
schema="narrow",
180+
show_progress=False,
181+
)
182+
183+
deprecation_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)]
184+
self.assertEqual(len(deprecation_warnings), 1)
185+
self.assertIn("narrow", str(deprecation_warnings[0].message))
186+
self.assertIn("wide", str(deprecation_warnings[0].message))
187+
188+
@patch('slicks.discovery.get_influx_client')
189+
@patch('slicks.discovery.config')
190+
def test_raises_on_permanent_error(self, mock_config, mock_client_class):
191+
"""Should raise RuntimeError on auth failure."""
192+
self._make_narrow_config(mock_config)
193+
194+
mock_client = MagicMock()
195+
mock_client.query.side_effect = Exception("Unauthorized: invalid token")
196+
mock_client_class.return_value = mock_client
197+
198+
with warnings.catch_warnings():
199+
warnings.simplefilter("ignore", DeprecationWarning)
200+
with self.assertRaises(RuntimeError):
201+
discover_sensors(
202+
start_time=datetime(2025, 1, 1),
203+
end_time=datetime(2025, 1, 2),
204+
schema="narrow",
205+
show_progress=False,
206+
)
207+
208+
@patch('slicks.discovery.get_influx_client')
209+
@patch('slicks.discovery.config')
210+
def test_default_chunk_size_is_7_days(self, mock_config, mock_client_class):
211+
"""Default chunk_size_days=7 should produce 2 chunks for 10-day range."""
212+
self._make_narrow_config(mock_config)
213+
214+
mock_table = MagicMock()
215+
mock_table.num_rows = 0
216+
mock_client = MagicMock()
217+
mock_client.query.return_value = mock_table
218+
mock_client_class.return_value = mock_client
219+
220+
with warnings.catch_warnings():
221+
warnings.simplefilter("ignore", DeprecationWarning)
222+
discover_sensors(
223+
start_time=datetime(2025, 1, 1),
224+
end_time=datetime(2025, 1, 11),
225+
schema="narrow",
226+
show_progress=False,
227+
)
228+
229+
# 10-day range with 7-day chunks = 2 chunks, each triggers at least 1 query
230+
self.assertGreaterEqual(mock_client.query.call_count, 2)
231+
232+
181233
if __name__ == '__main__':
182234
unittest.main()

0 commit comments

Comments
 (0)