Skip to content

Commit 148fb87

Browse files
fix: typo + deletion endpoint + uppercase validation
1 parent a13a662 commit 148fb87

4 files changed

Lines changed: 127 additions & 4 deletions

File tree

src/core/datalake.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,73 @@ def get_data_range(instrument: str, timeframe: str) -> Optional[dict]:
621621
return None
622622

623623

624+
def delete_ohlc_data(
625+
instrument: str,
626+
timeframe: Optional[str] = None,
627+
start=None,
628+
end=None,
629+
) -> int:
630+
"""
631+
Delete rows from ohlc_data matching instrument [+ timeframe] [+ window).
632+
Window is half-open [start, end). Returns rows deleted.
633+
"""
634+
instrument = validate_instrument(instrument)
635+
if timeframe is not None:
636+
timeframe = validate_timeframe(timeframe)
637+
638+
clauses = ["instrument = ?"]
639+
params: list = [instrument]
640+
if timeframe is not None:
641+
clauses.append("timeframe = ?")
642+
params.append(timeframe)
643+
if start is not None:
644+
clauses.append("timestamp >= ?")
645+
params.append(pd.Timestamp(start).tz_localize(None) if pd.Timestamp(start).tz is None else pd.Timestamp(start).tz_convert("UTC").tz_localize(None))
646+
if end is not None:
647+
clauses.append("timestamp < ?")
648+
params.append(pd.Timestamp(end).tz_localize(None) if pd.Timestamp(end).tz is None else pd.Timestamp(end).tz_convert("UTC").tz_localize(None))
649+
where = " AND ".join(clauses)
650+
651+
with _write_tx_lock:
652+
con = _get_shared_connection()
653+
before = con.execute(f"SELECT COUNT(*) FROM ohlc_data WHERE {where}", params).fetchone()[0]
654+
con.execute(f"DELETE FROM ohlc_data WHERE {where}", params)
655+
656+
logger.info("Deleted OHLC rows", extra={
657+
"instrument": instrument, "timeframe": timeframe,
658+
"start": str(start) if start else None, "end": str(end) if end else None,
659+
"rows": before,
660+
})
661+
return before
662+
663+
664+
def delete_tick_data(instrument: str, start=None, end=None) -> int:
665+
"""Delete rows from tick_data matching instrument [+ window). Returns rows deleted."""
666+
instrument = validate_instrument(instrument)
667+
668+
clauses = ["instrument = ?"]
669+
params: list = [instrument]
670+
if start is not None:
671+
clauses.append("timestamp >= ?")
672+
params.append(pd.Timestamp(start).tz_localize(None) if pd.Timestamp(start).tz is None else pd.Timestamp(start).tz_convert("UTC").tz_localize(None))
673+
if end is not None:
674+
clauses.append("timestamp < ?")
675+
params.append(pd.Timestamp(end).tz_localize(None) if pd.Timestamp(end).tz is None else pd.Timestamp(end).tz_convert("UTC").tz_localize(None))
676+
where = " AND ".join(clauses)
677+
678+
with _write_tx_lock:
679+
con = _get_shared_connection()
680+
before = con.execute(f"SELECT COUNT(*) FROM tick_data WHERE {where}", params).fetchone()[0]
681+
con.execute(f"DELETE FROM tick_data WHERE {where}", params)
682+
683+
logger.info("Deleted tick rows", extra={
684+
"instrument": instrument,
685+
"start": str(start) if start else None, "end": str(end) if end else None,
686+
"rows": before,
687+
})
688+
return before
689+
690+
624691
def get_database_stats() -> dict:
625692
"""Get overall database statistics."""
626693
with get_db_connection() as con:

src/routes/instruments.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
"""Instruments routes - list instruments and timeframes from DuckDB."""
2+
from datetime import datetime
23
from typing import Optional
34

45
from fastapi import APIRouter, Depends, Query, HTTPException
56

67
from src.config import ALLOW_PUBLIC_READS
78
from src.core.database import User
8-
from src.core.datalake import list_instruments, list_timeframes, get_data_range, list_tick_instruments, get_tick_coverage
9-
from src.services.validators import validate_instrument
9+
from src.core.datalake import (
10+
list_instruments,
11+
list_timeframes,
12+
get_data_range,
13+
list_tick_instruments,
14+
get_tick_coverage,
15+
delete_ohlc_data,
16+
delete_tick_data,
17+
)
18+
from src.services.validators import validate_instrument, validate_timeframe
1019
from src.auth.auth import ScopedAuth
1120

1221
router = APIRouter()
@@ -62,6 +71,53 @@ def get_instrument_detail(
6271
return {"symbol": symbol, "timeframes": coverage}
6372

6473

74+
@router.delete("/instruments/{symbol}")
75+
def delete_instrument_data(
76+
symbol: str,
77+
timeframe: Optional[str] = Query(None, description="If set, only delete this timeframe (or 'TICK' for ticks)."),
78+
start: Optional[datetime] = Query(None, description="Half-open window start (UTC). Inclusive."),
79+
end: Optional[datetime] = Query(None, description="Half-open window end (UTC). Exclusive."),
80+
include_ticks: bool = Query(True, description="Also delete tick_data when timeframe is unset."),
81+
confirm: bool = Query(False, description="Must be true — guard against accidental DELETE."),
82+
current_user: User = Depends(ScopedAuth("admin")),
83+
):
84+
"""
85+
Delete OHLC and/or tick rows for a symbol. Admin-only.
86+
87+
- No `timeframe`: wipes every OHLC timeframe for the symbol; also wipes ticks
88+
unless `include_ticks=false`.
89+
- `timeframe=TICK`: only ticks.
90+
- `timeframe=M1` (etc): only that OHLC timeframe.
91+
- `start`/`end` scope by window; omit for full range.
92+
"""
93+
if not confirm:
94+
raise HTTPException(status_code=400, detail="Pass confirm=true to actually delete.")
95+
96+
symbol = validate_instrument(symbol)
97+
98+
deleted = {}
99+
if timeframe is None:
100+
deleted["ohlc"] = delete_ohlc_data(symbol, start=start, end=end)
101+
if include_ticks:
102+
deleted["ticks"] = delete_tick_data(symbol, start=start, end=end)
103+
elif timeframe.upper() == "TICK":
104+
deleted["ticks"] = delete_tick_data(symbol, start=start, end=end)
105+
else:
106+
tf = validate_timeframe(timeframe)
107+
deleted["ohlc"] = delete_ohlc_data(symbol, timeframe=tf, start=start, end=end)
108+
109+
return {
110+
"status": "ok",
111+
"symbol": symbol,
112+
"timeframe": timeframe,
113+
"window": {
114+
"start": start.isoformat() if start else None,
115+
"end": end.isoformat() if end else None,
116+
},
117+
"deleted": deleted,
118+
}
119+
120+
65121
@router.get("/timeframes")
66122
def get_timeframes(
67123
instrument: str | None = Query(None),

src/services/validators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def validate_instrument(instrument: str) -> str:
3737
detail="Invalid instrument name: must contain only alphanumeric characters, underscores, hyphens, and ampersands"
3838
)
3939

40-
return instrument
40+
return instrument.upper()
4141

4242

4343
def validate_timeframe(timeframe: str) -> str:

web/src/pages/index.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const year = new Date().getFullYear();
2626
Since 2025.
2727
</div>
2828
<div class="flex items-center gap-6">
29-
<a href="https://github.com/lucasguerin/datalake-api" target="_blank" rel="noopener" class="hover:text-ink-200 transition-colors">GitHub</a>
29+
<a href="https://github.com/lucas-guerin-44/datalake-api" target="_blank" rel="noopener" class="hover:text-ink-200 transition-colors">GitHub</a>
3030
<a href="mailto:guerin.lucas44@gmail.com" class="hover:text-ink-200 transition-colors">guerin.lucas44@gmail.com</a>
3131
</div>
3232
</div>

0 commit comments

Comments
 (0)