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.
- liteseries-1.0.0/.gitattributes +2 -0
- liteseries-1.0.0/.gitconfig +0 -0
- liteseries-1.0.0/.gitignore +25 -0
- liteseries-1.0.0/LICENSE +21 -0
- liteseries-1.0.0/Liteseries-1.png +0 -0
- liteseries-1.0.0/PKG-INFO +250 -0
- liteseries-1.0.0/README.md +216 -0
- liteseries-1.0.0/demo.py +138 -0
- liteseries-1.0.0/liteseries/__init__.py +5 -0
- liteseries-1.0.0/liteseries/_handlers.py +308 -0
- liteseries-1.0.0/liteseries/_sql.py +104 -0
- liteseries-1.0.0/liteseries/_util.py +153 -0
- liteseries-1.0.0/liteseries/nexus.md +11 -0
- liteseries-1.0.0/liteseries/py.typed +0 -0
- liteseries-1.0.0/pyproject.toml +98 -0
- liteseries-1.0.0/radon.cfg +31 -0
- liteseries-1.0.0/review.md +15 -0
- liteseries-1.0.0/test-results.md +5 -0
- liteseries-1.0.0/tests/integration/test_imports.py +9 -0
- liteseries-1.0.0/tests/integration/test_public_api.py +817 -0
- liteseries-1.0.0/tests/integration/test_sql_names.py +60 -0
- liteseries-1.0.0/tests/report/.gitkeep +1 -0
- liteseries-1.0.0/tests/report/pytest/2026-04-13 19-43-36 UTC/coverage.txt +12 -0
- liteseries-1.0.0/tests/report/pytest/2026-04-13 19-43-36 UTC/failures.txt +1 -0
- liteseries-1.0.0/tests/report/pytest/2026-04-13 23-36-41 UTC/coverage.txt +11 -0
- liteseries-1.0.0/tests/report/pytest/2026-04-13 23-36-41 UTC/failures.txt +1 -0
- liteseries-1.0.0/tests/report/pytest/2026-04-15 20-28-03 UTC/coverage.txt +11 -0
- liteseries-1.0.0/tests/report/pytest/2026-04-15 20-28-03 UTC/failures.txt +1 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-05 19-28-54 UTC/coverage.txt +11 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-05 19-28-54 UTC/failures.txt +1 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-05 19-55-47 UTC/coverage.txt +11 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-05 19-55-47 UTC/failures.txt +1 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-07 16-24-39 UTC/coverage.txt +11 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-07 16-24-39 UTC/failures.txt +1 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-08 20-10-25 UTC/coverage.txt +11 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-08 20-10-25 UTC/failures.txt +1 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-08 20-22-32 UTC/coverage.txt +11 -0
- liteseries-1.0.0/tests/report/pytest/2026-06-08 20-22-32 UTC/failures.txt +1 -0
- liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/cc.md +29 -0
- liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/flagged.md +8 -0
- liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/hal.md +371 -0
- liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/mi.md +4 -0
- liteseries-1.0.0/tests/report/radon/2026-04-14 01-41-55 UTC/raw.md +60 -0
- liteseries-1.0.0/tests/unit/.gitkeep +1 -0
- liteseries-1.0.0/typings/liteseries/__init__.pyi +3 -0
- liteseries-1.0.0/typings/liteseries/_handlers.pyi +24 -0
- liteseries-1.0.0/typings/liteseries/_sql.pyi +1 -0
- liteseries-1.0.0/typings/liteseries/_util.pyi +1 -0
|
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
|
liteseries-1.0.0/LICENSE
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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`.
|
liteseries-1.0.0/demo.py
ADDED
|
@@ -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()
|