liteseries 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. liteseries-1.0.0/.gitattributes +2 -0
  2. liteseries-1.0.0/.gitconfig +0 -0
  3. liteseries-1.0.0/.gitignore +25 -0
  4. liteseries-1.0.0/LICENSE +21 -0
  5. liteseries-1.0.0/Liteseries-1.png +0 -0
  6. liteseries-1.0.0/PKG-INFO +250 -0
  7. liteseries-1.0.0/README.md +216 -0
  8. liteseries-1.0.0/demo.py +138 -0
  9. liteseries-1.0.0/liteseries/__init__.py +5 -0
  10. liteseries-1.0.0/liteseries/_handlers.py +308 -0
  11. liteseries-1.0.0/liteseries/_sql.py +104 -0
  12. liteseries-1.0.0/liteseries/_util.py +153 -0
  13. liteseries-1.0.0/liteseries/nexus.md +11 -0
  14. liteseries-1.0.0/liteseries/py.typed +0 -0
  15. liteseries-1.0.0/pyproject.toml +98 -0
  16. liteseries-1.0.0/radon.cfg +31 -0
  17. liteseries-1.0.0/review.md +15 -0
  18. liteseries-1.0.0/test-results.md +5 -0
  19. liteseries-1.0.0/tests/integration/test_imports.py +9 -0
  20. liteseries-1.0.0/tests/integration/test_public_api.py +817 -0
  21. liteseries-1.0.0/tests/integration/test_sql_names.py +60 -0
  22. liteseries-1.0.0/tests/report/.gitkeep +1 -0
  23. liteseries-1.0.0/tests/report/pytest/2026-04-13 19-43-36 UTC/coverage.txt +12 -0
  24. liteseries-1.0.0/tests/report/pytest/2026-04-13 19-43-36 UTC/failures.txt +1 -0
  25. liteseries-1.0.0/tests/report/pytest/2026-04-13 23-36-41 UTC/coverage.txt +11 -0
  26. liteseries-1.0.0/tests/report/pytest/2026-04-13 23-36-41 UTC/failures.txt +1 -0
  27. liteseries-1.0.0/tests/report/pytest/2026-04-15 20-28-03 UTC/coverage.txt +11 -0
  28. liteseries-1.0.0/tests/report/pytest/2026-04-15 20-28-03 UTC/failures.txt +1 -0
  29. liteseries-1.0.0/tests/report/pytest/2026-06-05 19-28-54 UTC/coverage.txt +11 -0
  30. liteseries-1.0.0/tests/report/pytest/2026-06-05 19-28-54 UTC/failures.txt +1 -0
  31. liteseries-1.0.0/tests/report/pytest/2026-06-05 19-55-47 UTC/coverage.txt +11 -0
  32. liteseries-1.0.0/tests/report/pytest/2026-06-05 19-55-47 UTC/failures.txt +1 -0
  33. liteseries-1.0.0/tests/report/pytest/2026-06-07 16-24-39 UTC/coverage.txt +11 -0
  34. liteseries-1.0.0/tests/report/pytest/2026-06-07 16-24-39 UTC/failures.txt +1 -0
  35. liteseries-1.0.0/tests/report/pytest/2026-06-08 20-10-25 UTC/coverage.txt +11 -0
  36. liteseries-1.0.0/tests/report/pytest/2026-06-08 20-10-25 UTC/failures.txt +1 -0
  37. liteseries-1.0.0/tests/report/pytest/2026-06-08 20-22-32 UTC/coverage.txt +11 -0
  38. liteseries-1.0.0/tests/report/pytest/2026-06-08 20-22-32 UTC/failures.txt +1 -0
  39. liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/cc.md +29 -0
  40. liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/flagged.md +8 -0
  41. liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/hal.md +371 -0
  42. liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/mi.md +4 -0
  43. liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/raw.md +60 -0
  44. liteseries-1.0.0/tests/unit/.gitkeep +1 -0
  45. liteseries-1.0.0/typings/liteseries/__init__.pyi +3 -0
  46. liteseries-1.0.0/typings/liteseries/_handlers.pyi +24 -0
  47. liteseries-1.0.0/typings/liteseries/_sql.pyi +1 -0
  48. liteseries-1.0.0/typings/liteseries/_util.pyi +1 -0
@@ -0,0 +1,2 @@
1
+ # *.ipynb linguist-documentation
2
+ *.ipynb linguist-detectable=false
File without changes
@@ -0,0 +1,25 @@
1
+ .idea
2
+ *.iml
3
+ out
4
+ gen
5
+ **/__pycache__/
6
+ **/*.py[cod]
7
+ venv
8
+ .venv
9
+ testing.ipynb
10
+ testing.py
11
+ test.ipynb
12
+ test.py
13
+ **/AGENTS.md
14
+ *.egg-info/
15
+ uv.lock
16
+ .coverage
17
+ .obsidian
18
+ dist/
19
+ **/html
20
+ progress.md
21
+ nexus-task.md
22
+ pyproject_temp.toml
23
+ temp
24
+ liteseries_db.sqlite
25
+ scratch.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Charles Marks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Binary file
@@ -0,0 +1,250 @@
1
+ Metadata-Version: 2.4
2
+ Name: liteseries
3
+ Version: 1.0.0
4
+ Summary: SQLite-backed local caching helpers for time series data.
5
+ Author-email: Charles Marks <charlesmarksco@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Charles Marks
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Classifier: License :: OSI Approved :: MIT License
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: adbc-driver-sqlite
31
+ Requires-Dist: pyarrow
32
+ Requires-Dist: python-dateutil
33
+ Description-Content-Type: text/markdown
34
+
35
+ ![Liteseries](Liteseries-1.png)
36
+
37
+ An SQLite-backed replicator and cache for timeseries data.
38
+
39
+ Liteseries works as an invisible layer around a timeseries endpoint. Your function still returns the rows you would
40
+ normally get from the vendor, while liteseries stores them in SQLite for the next matching call. When a request reaches
41
+ past the saved range, it reads the local slice first and asks the vendor only for the missing forward period. Liteseries
42
+ is stable for single-threaded use, with parallel reads available where the Arrow backend supports them.
43
+
44
+ ```python
45
+ from liteseries import close_ls, launch_ls, ls_cache, threadpool_shutdown_ls
46
+ ```
47
+
48
+ - `launch_ls()` opens the local SQLite/ADBC runtime for the current thread.
49
+ - `ls_cache(...)` decorates endpoint functions that return `pyarrow.Table`
50
+ objects.
51
+ - `close_ls()` closes the runtime connection when your process is done with it.
52
+ - `threadpool_shutdown_ls(...)` for use in a multithreaded environment.
53
+
54
+ ## Usage
55
+
56
+ Install the package, then call `launch_ls()` once before cached functions run:
57
+
58
+ ```python
59
+ from __future__ import annotations
60
+
61
+ from datetime import UTC, datetime, time, timedelta
62
+
63
+ import pyarrow as pa
64
+
65
+ from liteseries import close_ls, launch_ls, ls_cache
66
+
67
+
68
+ def unix_micros(year: int, month: int, day: int) -> int:
69
+ """Return a UTC timestamp in the microsecond units liteseries stores."""
70
+ return int(datetime(year, month, day, tzinfo=UTC).timestamp() * 1_000_000)
71
+
72
+
73
+ launch_ls()
74
+ ```
75
+
76
+ By default, `launch_ls()` looks for an existing `.sqlite` file near the entry
77
+ script, prefers one with `liteseries` in its name, and otherwise creates
78
+ `liteseries_db.sqlite`. Pass a path when you want to choose the file yourself:
79
+
80
+ ```python
81
+ launch_ls("cache/prices.sqlite")
82
+ ```
83
+
84
+ You can also set `LITESERIES_DB` before launch when configuration belongs in the
85
+ environment:
86
+
87
+ ```powershell
88
+ $env:LITESERIES_DB = "D:\market-cache\prices.sqlite"
89
+ ```
90
+
91
+ ### Wrap An Endpoint
92
+
93
+ Your endpoint function should accept keyword arguments for the requested time
94
+ range and key values, then return a `pyarrow.Table`. The time column is expected
95
+ to use Unix microseconds, and `None` means an open-ended side of the range.
96
+
97
+ This example uses a local data source so the shape is easy to see. A real
98
+ function can call an HTTP API, a vendor SDK, or a file reader.
99
+
100
+ ```python
101
+ def vendor_prices(start, end, symbol, interval) -> pa.Table:
102
+ """Fetch rows from the source system and return only endpoint columns."""
103
+ rows = [
104
+ (unix_micros(2026, 1, 2), 101.25, 102.10),
105
+ (unix_micros(2026, 1, 3), 102.00, 103.40),
106
+ (unix_micros(2026, 1, 4), 103.25, 103.05),
107
+ ]
108
+
109
+ if start is not None:
110
+ rows = [row for row in rows if row[0] >= start]
111
+ if end is not None:
112
+ rows = [row for row in rows if row[0] <= end]
113
+
114
+ return pa.table(
115
+ {
116
+ "ts": [row[0] for row in rows],
117
+ "open": [row[1] for row in rows],
118
+ "close": [row[2] for row in rows],
119
+ }
120
+ )
121
+ ```
122
+
123
+ Decorate it with `ls_cache`. `columns` is the full SQLite schema, including key
124
+ columns that may come from function arguments instead of the returned Arrow
125
+ table. `column_keys` identify separate series inside one table, while
126
+ `table_keys` can split families such as intervals into separate SQLite tables.
127
+
128
+ ```python
129
+ @ls_cache(
130
+ columns=("symbol", "ts", "open", "close"),
131
+ time_keys=("start", "end"),
132
+ time_col="ts",
133
+ column_keys=("symbol",),
134
+ out_cols=("ts", "open", "close"),
135
+ table="prices",
136
+ refresh_period=timedelta(days=1),
137
+ active_in=time(0, tzinfo=UTC),
138
+ )
139
+ def prices(start, end, symbol):
140
+ """Return cached prices, refreshing from the endpoint when needed."""
141
+ return vendor_prices(start, end, symbol, interval="1d")
142
+ ```
143
+
144
+ Now call the wrapped function with keyword arguments:
145
+
146
+ ```python
147
+ try:
148
+ first = prices(
149
+ start=unix_micros(2026, 1, 2),
150
+ end=unix_micros(2026, 1, 4),
151
+ symbol="MSFT",
152
+ )
153
+
154
+ second = prices(
155
+ start=unix_micros(2026, 1, 2),
156
+ end=unix_micros(2026, 1, 4),
157
+ symbol="MSFT",
158
+ )
159
+
160
+ assert second.equals(first)
161
+ finally:
162
+ close_ls()
163
+ ```
164
+
165
+ On the first call, liteseries creates the data table and its metadata table,
166
+ fills missing key columns, writes the Arrow rows, and returns the requested
167
+ slice. On later calls, it serves rows from SQLite unless the request reaches
168
+ past the cached freshness boundary.
169
+
170
+ ### Refresh Windows
171
+
172
+ `refresh_period` and `active_in` tell liteseries when it is worth checking the
173
+ endpoint again.
174
+
175
+ For daily or slower data, pass one `time` that marks when the latest period is
176
+ expected to be complete:
177
+
178
+ ```python
179
+ @ls_cache(
180
+ columns=("symbol", "ts", "open", "close"),
181
+ time_keys=("start", "end"),
182
+ time_col="ts",
183
+ column_keys=("symbol",),
184
+ table="daily_prices",
185
+ refresh_period=timedelta(days=1),
186
+ active_in=time(16, 1, tzinfo=UTC),
187
+ )
188
+ def daily_prices(start, end, symbol):
189
+ """Cache daily bars after the market close boundary has passed."""
190
+ return vendor_prices(start, end, symbol, interval="1d")
191
+ ```
192
+
193
+ For intraday data, pass a `(start_time, end_time)` window. Liteseries advances
194
+ the freshness boundary by `refresh_period` steps inside that window:
195
+
196
+ ```python
197
+ @ls_cache(
198
+ columns=("symbol", "ts", "open", "close"),
199
+ time_keys=("start", "end"),
200
+ time_col="ts",
201
+ column_keys=("symbol",),
202
+ table_keys=("interval",),
203
+ out_cols=("ts", "open", "close"),
204
+ table="intraday_prices",
205
+ refresh_period=timedelta(minutes=5),
206
+ active_in=(time(9, 30, tzinfo=UTC), time(16, 0, tzinfo=UTC)),
207
+ )
208
+ def intraday_prices(start, end, symbol, interval):
209
+ """Cache one interval per SQLite table, keyed by symbol within each table."""
210
+ return vendor_prices(start, end, symbol, interval=interval)
211
+ ```
212
+
213
+ ### Columns And Names
214
+
215
+ Use plain Python-style column and table names when you can. That is the default
216
+ fast path. If your endpoint already produces names with spaces or quotes, set
217
+ `LITESERIES_PROTECTNAMES=true` before importing liteseries so SQL identifiers
218
+ are double-quoted:
219
+
220
+ ```powershell
221
+ $env:LITESERIES_PROTECTNAMES = "true"
222
+ ```
223
+
224
+ `out_cols` controls the columns returned by the decorated function. This is handy
225
+ when the database needs key columns such as `symbol`, but callers only want the
226
+ time-series values.
227
+
228
+ ### Empty Tails
229
+
230
+ Some providers stop returning rows after a symbol expires or a contract ends.
231
+ Pass `expires_after` when liteseries should stop asking forward for that key
232
+ after repeated empty tail checks:
233
+
234
+ ```python
235
+ @ls_cache(
236
+ columns=("symbol", "ts", "open", "close"),
237
+ time_keys=("start", "end"),
238
+ time_col="ts",
239
+ column_keys=("symbol",),
240
+ table="expired_prices",
241
+ refresh_period=timedelta(hours=1),
242
+ active_in=(time(0, tzinfo=UTC), time(23, 59, tzinfo=UTC)),
243
+ expires_after=timedelta(days=7),
244
+ )
245
+ def expired_prices(start, end, symbol):
246
+ """Cache sparse series without querying forever beyond the final row."""
247
+ return vendor_prices(start, end, symbol, interval="1h")
248
+ ```
249
+
250
+ For a live provider example with pandas/yfinance conversion, see `demo.py`.
@@ -0,0 +1,216 @@
1
+ ![Liteseries](Liteseries-1.png)
2
+
3
+ An SQLite-backed replicator and cache for timeseries data.
4
+
5
+ Liteseries works as an invisible layer around a timeseries endpoint. Your function still returns the rows you would
6
+ normally get from the vendor, while liteseries stores them in SQLite for the next matching call. When a request reaches
7
+ past the saved range, it reads the local slice first and asks the vendor only for the missing forward period. Liteseries
8
+ is stable for single-threaded use, with parallel reads available where the Arrow backend supports them.
9
+
10
+ ```python
11
+ from liteseries import close_ls, launch_ls, ls_cache, threadpool_shutdown_ls
12
+ ```
13
+
14
+ - `launch_ls()` opens the local SQLite/ADBC runtime for the current thread.
15
+ - `ls_cache(...)` decorates endpoint functions that return `pyarrow.Table`
16
+ objects.
17
+ - `close_ls()` closes the runtime connection when your process is done with it.
18
+ - `threadpool_shutdown_ls(...)` for use in a multithreaded environment.
19
+
20
+ ## Usage
21
+
22
+ Install the package, then call `launch_ls()` once before cached functions run:
23
+
24
+ ```python
25
+ from __future__ import annotations
26
+
27
+ from datetime import UTC, datetime, time, timedelta
28
+
29
+ import pyarrow as pa
30
+
31
+ from liteseries import close_ls, launch_ls, ls_cache
32
+
33
+
34
+ def unix_micros(year: int, month: int, day: int) -> int:
35
+ """Return a UTC timestamp in the microsecond units liteseries stores."""
36
+ return int(datetime(year, month, day, tzinfo=UTC).timestamp() * 1_000_000)
37
+
38
+
39
+ launch_ls()
40
+ ```
41
+
42
+ By default, `launch_ls()` looks for an existing `.sqlite` file near the entry
43
+ script, prefers one with `liteseries` in its name, and otherwise creates
44
+ `liteseries_db.sqlite`. Pass a path when you want to choose the file yourself:
45
+
46
+ ```python
47
+ launch_ls("cache/prices.sqlite")
48
+ ```
49
+
50
+ You can also set `LITESERIES_DB` before launch when configuration belongs in the
51
+ environment:
52
+
53
+ ```powershell
54
+ $env:LITESERIES_DB = "D:\market-cache\prices.sqlite"
55
+ ```
56
+
57
+ ### Wrap An Endpoint
58
+
59
+ Your endpoint function should accept keyword arguments for the requested time
60
+ range and key values, then return a `pyarrow.Table`. The time column is expected
61
+ to use Unix microseconds, and `None` means an open-ended side of the range.
62
+
63
+ This example uses a local data source so the shape is easy to see. A real
64
+ function can call an HTTP API, a vendor SDK, or a file reader.
65
+
66
+ ```python
67
+ def vendor_prices(start, end, symbol, interval) -> pa.Table:
68
+ """Fetch rows from the source system and return only endpoint columns."""
69
+ rows = [
70
+ (unix_micros(2026, 1, 2), 101.25, 102.10),
71
+ (unix_micros(2026, 1, 3), 102.00, 103.40),
72
+ (unix_micros(2026, 1, 4), 103.25, 103.05),
73
+ ]
74
+
75
+ if start is not None:
76
+ rows = [row for row in rows if row[0] >= start]
77
+ if end is not None:
78
+ rows = [row for row in rows if row[0] <= end]
79
+
80
+ return pa.table(
81
+ {
82
+ "ts": [row[0] for row in rows],
83
+ "open": [row[1] for row in rows],
84
+ "close": [row[2] for row in rows],
85
+ }
86
+ )
87
+ ```
88
+
89
+ Decorate it with `ls_cache`. `columns` is the full SQLite schema, including key
90
+ columns that may come from function arguments instead of the returned Arrow
91
+ table. `column_keys` identify separate series inside one table, while
92
+ `table_keys` can split families such as intervals into separate SQLite tables.
93
+
94
+ ```python
95
+ @ls_cache(
96
+ columns=("symbol", "ts", "open", "close"),
97
+ time_keys=("start", "end"),
98
+ time_col="ts",
99
+ column_keys=("symbol",),
100
+ out_cols=("ts", "open", "close"),
101
+ table="prices",
102
+ refresh_period=timedelta(days=1),
103
+ active_in=time(0, tzinfo=UTC),
104
+ )
105
+ def prices(start, end, symbol):
106
+ """Return cached prices, refreshing from the endpoint when needed."""
107
+ return vendor_prices(start, end, symbol, interval="1d")
108
+ ```
109
+
110
+ Now call the wrapped function with keyword arguments:
111
+
112
+ ```python
113
+ try:
114
+ first = prices(
115
+ start=unix_micros(2026, 1, 2),
116
+ end=unix_micros(2026, 1, 4),
117
+ symbol="MSFT",
118
+ )
119
+
120
+ second = prices(
121
+ start=unix_micros(2026, 1, 2),
122
+ end=unix_micros(2026, 1, 4),
123
+ symbol="MSFT",
124
+ )
125
+
126
+ assert second.equals(first)
127
+ finally:
128
+ close_ls()
129
+ ```
130
+
131
+ On the first call, liteseries creates the data table and its metadata table,
132
+ fills missing key columns, writes the Arrow rows, and returns the requested
133
+ slice. On later calls, it serves rows from SQLite unless the request reaches
134
+ past the cached freshness boundary.
135
+
136
+ ### Refresh Windows
137
+
138
+ `refresh_period` and `active_in` tell liteseries when it is worth checking the
139
+ endpoint again.
140
+
141
+ For daily or slower data, pass one `time` that marks when the latest period is
142
+ expected to be complete:
143
+
144
+ ```python
145
+ @ls_cache(
146
+ columns=("symbol", "ts", "open", "close"),
147
+ time_keys=("start", "end"),
148
+ time_col="ts",
149
+ column_keys=("symbol",),
150
+ table="daily_prices",
151
+ refresh_period=timedelta(days=1),
152
+ active_in=time(16, 1, tzinfo=UTC),
153
+ )
154
+ def daily_prices(start, end, symbol):
155
+ """Cache daily bars after the market close boundary has passed."""
156
+ return vendor_prices(start, end, symbol, interval="1d")
157
+ ```
158
+
159
+ For intraday data, pass a `(start_time, end_time)` window. Liteseries advances
160
+ the freshness boundary by `refresh_period` steps inside that window:
161
+
162
+ ```python
163
+ @ls_cache(
164
+ columns=("symbol", "ts", "open", "close"),
165
+ time_keys=("start", "end"),
166
+ time_col="ts",
167
+ column_keys=("symbol",),
168
+ table_keys=("interval",),
169
+ out_cols=("ts", "open", "close"),
170
+ table="intraday_prices",
171
+ refresh_period=timedelta(minutes=5),
172
+ active_in=(time(9, 30, tzinfo=UTC), time(16, 0, tzinfo=UTC)),
173
+ )
174
+ def intraday_prices(start, end, symbol, interval):
175
+ """Cache one interval per SQLite table, keyed by symbol within each table."""
176
+ return vendor_prices(start, end, symbol, interval=interval)
177
+ ```
178
+
179
+ ### Columns And Names
180
+
181
+ Use plain Python-style column and table names when you can. That is the default
182
+ fast path. If your endpoint already produces names with spaces or quotes, set
183
+ `LITESERIES_PROTECTNAMES=true` before importing liteseries so SQL identifiers
184
+ are double-quoted:
185
+
186
+ ```powershell
187
+ $env:LITESERIES_PROTECTNAMES = "true"
188
+ ```
189
+
190
+ `out_cols` controls the columns returned by the decorated function. This is handy
191
+ when the database needs key columns such as `symbol`, but callers only want the
192
+ time-series values.
193
+
194
+ ### Empty Tails
195
+
196
+ Some providers stop returning rows after a symbol expires or a contract ends.
197
+ Pass `expires_after` when liteseries should stop asking forward for that key
198
+ after repeated empty tail checks:
199
+
200
+ ```python
201
+ @ls_cache(
202
+ columns=("symbol", "ts", "open", "close"),
203
+ time_keys=("start", "end"),
204
+ time_col="ts",
205
+ column_keys=("symbol",),
206
+ table="expired_prices",
207
+ refresh_period=timedelta(hours=1),
208
+ active_in=(time(0, tzinfo=UTC), time(23, 59, tzinfo=UTC)),
209
+ expires_after=timedelta(days=7),
210
+ )
211
+ def expired_prices(start, end, symbol):
212
+ """Cache sparse series without querying forever beyond the final row."""
213
+ return vendor_prices(start, end, symbol, interval="1h")
214
+ ```
215
+
216
+ For a live provider example with pandas/yfinance conversion, see `demo.py`.
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from datetime import UTC
5
+ from datetime import datetime
6
+ from datetime import time
7
+ from datetime import timedelta
8
+
9
+ import pandas as pd
10
+ import pyarrow as pa
11
+ import yfinance as yf
12
+ from pyarrow import concat_tables
13
+
14
+ from liteseries import close_ls
15
+ from liteseries import launch_ls
16
+ from liteseries import ls_cache
17
+
18
+ os.environ["LITESERIES_PROTECTNAMES"]='true'
19
+
20
+ launch_ls()
21
+
22
+
23
+ def dt_micros(year: int, month: int, day: int) -> int:
24
+ return int(datetime(year, month, day, tzinfo=UTC).timestamp() * 1_000_000)
25
+
26
+
27
+ def yf_to_arrow(frame: pd.DataFrame|None) -> pa.Table|None:
28
+ if frame is None: return None
29
+ frame = frame.rename_axis("Date").reset_index()
30
+ frame["ts"] = pd.to_datetime(frame.pop("Date"),utc=True).astype("int64")*1_000_000
31
+ frame["Volume"] = frame["Volume"].fillna(0).astype("int64")
32
+ return pa.Table.from_pandas(frame, preserve_index=False)
33
+
34
+
35
+
36
+ def yf_prices_full(start, end, ticker, interval, auto_adjust=False) -> pa.Table|None:
37
+ start_dt = pd.Timestamp(start, unit="us", tz="UTC").to_pydatetime() if start is not None else None
38
+ end_dt = pd.Timestamp(end, unit="us", tz="UTC").to_pydatetime() + pd.Timedelta(days=1) if end is not None else None
39
+ now_utc = datetime.now(UTC)
40
+
41
+ def download_window(start_dt, end_dt)->pd.DataFrame|None:
42
+ return yf.download(
43
+ tickers=ticker,
44
+ start=start_dt,
45
+ end=end_dt,
46
+ interval=interval,
47
+ auto_adjust=auto_adjust,
48
+ actions=False,
49
+ progress=False,
50
+ threads=True,
51
+ multi_level_index=False,
52
+ )
53
+
54
+ if interval != "1m" and start_dt is None:
55
+ end_dt = None
56
+
57
+ if interval == "1m":
58
+ if start_dt is None:
59
+ start_dt = now_utc - timedelta(days=30)
60
+ if end_dt is None:
61
+ end_dt = now_utc
62
+
63
+ if end_dt - start_dt > timedelta(days=8):
64
+ chunks: list[pa.Table] = []
65
+ chunk_start = start_dt
66
+ while chunk_start < end_dt:
67
+ chunk_end = min(chunk_start + timedelta(days=8), end_dt)
68
+ frame = download_window(chunk_start, chunk_end)
69
+ if frame is not None and not frame.empty:
70
+ chunks.append(yf_to_arrow(frame)) # type: ignore
71
+ chunk_start = chunk_end
72
+ if len(chunks)!=0:
73
+ return concat_tables(chunks, promote_options="none")
74
+ if not chunks:
75
+ return yf_to_arrow(download_window(start_dt, end_dt))
76
+ return concat_tables(chunks, promote_options="none")
77
+
78
+ return yf_to_arrow(download_window(start_dt, end_dt))
79
+
80
+
81
+ @ls_cache(
82
+ columns=("Adj Close", "Close", "High", "Low", "Open", "Volume", "ts", "ticker"),
83
+ time_keys=("start", "end"),
84
+ time_col="ts",
85
+ column_keys=("ticker",),
86
+ table_keys=("interval",),
87
+ out_cols=("Adj Close", "Close", "High", "Low", "Open", "Volume", "ts"),
88
+ table="yf_prices",
89
+ refresh_period=timedelta(days=1),
90
+ active_in=time(0, tzinfo=UTC),
91
+ )
92
+ def yf_prices(start, end, ticker, interval):
93
+ return yf_prices_full(start, end, ticker, interval, auto_adjust=False)
94
+
95
+
96
+ @ls_cache(
97
+ columns=("Close", "High", "Low", "Open", "Volume", "ts", "ticker"),
98
+ time_keys=("start", "end"),
99
+ time_col="ts",
100
+ column_keys=("ticker",),
101
+ table_keys=("interval",),
102
+ out_cols=("Close", "High", "Low", "Open", "Volume", "ts"),
103
+ table="yf_prices_adj",
104
+ refresh_period=timedelta(days=1),
105
+ active_in=time(0, tzinfo=UTC),
106
+ )
107
+ def yf_prices_adj(start, end, ticker, interval):
108
+ return yf_prices_full(start, end, ticker, interval, auto_adjust=True)
109
+
110
+
111
+ def main() -> None:
112
+ try:
113
+ start = None #dt_micros(2026, 4, 8)
114
+ end = None #dt_micros(2026, 3, 25)
115
+ print(start, end)
116
+
117
+ raw_first = yf_prices(start=start, end=end, ticker="MSFT", interval="1m")
118
+ raw_second = yf_prices(start=start, end=end, ticker="MSFT", interval="1m")
119
+ adj_first = yf_prices_adj(start=start, end=end, ticker="MSFT", interval="5m")
120
+ adj_second = yf_prices_adj(start=start, end=end, ticker="MSFT", interval="5m")
121
+
122
+ print("raw columns:", raw_first.column_names)
123
+ print("raw rows:", raw_first.num_rows)
124
+ print("raw cached second call:", raw_second.num_rows == raw_first.num_rows)
125
+ print(raw_second)
126
+ print()
127
+ print("adjusted columns:", adj_first.column_names)
128
+ print("adjusted rows:", adj_first.num_rows)
129
+ print("adjusted cached second call:", adj_second.num_rows == adj_first.num_rows)
130
+ print(adj_second)
131
+
132
+ #print(yf_prices_full(start,end,"MSFT",'1m'))
133
+ finally:
134
+ close_ls()
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from ._handlers import close_ls, launch_ls, ls_cache, threadpool_shutdown_ls
4
+
5
+ __all__ = ["close_ls", "launch_ls", "ls_cache", "threadpool_shutdown_ls"]