small fixes
This commit is contained in:
parent
39d28d2e8d
commit
db9269f056
@ -69,7 +69,6 @@ def get_month():
|
||||
start = start + timedelta(days=1)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TGE_RDNScraper(EnergyPriceScraperBase, HasLogger):
|
||||
"""
|
||||
Scraper TGE RDN 'Notowania w systemie kursu jednolitego / fixing' z pliku Excel.
|
||||
|
||||
@ -12,5 +12,10 @@ if [ -f "$APP_DIR/requirements.txt" ]; then
|
||||
sudo -u energy "$APP_DIR/.venv/bin/pip" install -r "$APP_DIR/requirements.txt"
|
||||
fi
|
||||
|
||||
sudo install -m 0755 $APP_DIR/os/energy-price-scrapers-update.sh /usr/local/bin/energy-price-scrapers-update
|
||||
sudo install -m 0755 $APP_DIR/os/energy-price-scrapers.service /etc/systemd/system/energy-price-scrapers.service
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart energy-price-scrapers.service
|
||||
|
||||
echo "Updated & restarted."
|
||||
|
||||
181
rest.py
Normal file
181
rest.py
Normal file
@ -0,0 +1,181 @@
|
||||
# main.py
|
||||
# FastAPI app exposing /today, /tomorrow, and /day/{YYYY-MM-DD} endpoints.
|
||||
# No auth, no cache. Each request opens a short-lived psycopg3 connection (no pool).
|
||||
# Returns JSON with items as "(start,end]" ranges and NETTO PLN/kWh.
|
||||
|
||||
from __future__ import annotations
|
||||
from functools import lru_cache
|
||||
|
||||
import EnergyPriceScraperFactory
|
||||
import os
|
||||
from datetime import datetime, date, timedelta
|
||||
from typing import List, Literal
|
||||
|
||||
import psycopg
|
||||
from psycopg.rows import dict_row
|
||||
from fastapi import FastAPI, HTTPException, Query, Path, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from utils.time import WARSAW_TZ, UTC
|
||||
|
||||
PROVIDER = "TGE"
|
||||
KIND = "market_price"
|
||||
SIDE = "buy"
|
||||
BUYER = "end_user"
|
||||
SELLER = "market_index"
|
||||
|
||||
# ------------------------- Models -------------------------
|
||||
class PricePoint(BaseModel):
|
||||
from_utc: str # ISO8601 w UTC
|
||||
to_utc: str # ISO8601 w UTC
|
||||
price: float
|
||||
|
||||
class Range(BaseModel):
|
||||
from_utc: str # ISO8601 w UTC
|
||||
to_utc: str # ISO8601 w UTC
|
||||
|
||||
class PriceEnvelope(BaseModel):
|
||||
range: Range
|
||||
interval: Literal["(,]"] = "(ts_utc,ts_utc]" # left-open, right-closed
|
||||
data: List[PricePoint] = Field(default_factory=list)
|
||||
|
||||
# ------------------------- Utilities -------------------------
|
||||
|
||||
def _connect():
|
||||
return EnergyPriceScraperFactory.setup_db()
|
||||
|
||||
|
||||
def _local_day_bounds(day: date) -> tuple[datetime, datetime]:
|
||||
"""Return (start, end) of local Warsaw calendar day as aware datetimes."""
|
||||
start_local = datetime(day.year, day.month, day.day, 0, 0, 0, tzinfo=WARSAW_TZ)
|
||||
end_local = start_local + timedelta(days=1)
|
||||
return start_local, end_local
|
||||
|
||||
|
||||
@lru_cache(maxsize=100, typed=False)
|
||||
def _fetch_prices(start_dt: datetime, end_dt: datetime):
|
||||
if end_dt <= start_dt:
|
||||
raise HTTPException(status_code=400, detail="end must be > start")
|
||||
|
||||
sql = """
|
||||
SELECT ts_start, ts_end, price_pln_net
|
||||
FROM energy_prices
|
||||
WHERE provider = %(provider)s
|
||||
AND kind = %(kind)s
|
||||
AND side = %(side)s
|
||||
AND buyer = %(buyer)s
|
||||
AND seller = %(seller)s
|
||||
AND ts_start >= %(start)s
|
||||
AND ts_start < %(end)s
|
||||
ORDER BY ts_start
|
||||
"""
|
||||
|
||||
with _connect() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, {
|
||||
"provider": PROVIDER,
|
||||
"kind": KIND,
|
||||
"side": SIDE,
|
||||
"buyer": BUYER,
|
||||
"seller": SELLER,
|
||||
"start": start_dt,
|
||||
"end": end_dt,
|
||||
})
|
||||
rows = cur.fetchall()
|
||||
|
||||
points: List[dict] = []
|
||||
for r in rows:
|
||||
points.append({
|
||||
"from_utc": r[0].astimezone(UTC).isoformat(),
|
||||
"to_utc": r[1].astimezone(UTC).isoformat(),
|
||||
"price": float(r[2]),
|
||||
})
|
||||
return points
|
||||
|
||||
# ------------------------- App -------------------------
|
||||
app = FastAPI(title="Energy Prices API", version="1.1.0")
|
||||
|
||||
@app.get("/healthz")
|
||||
def healthz():
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/today", response_model=PriceEnvelope)
|
||||
def prices_today():
|
||||
today_local = datetime.now(WARSAW_TZ).date()
|
||||
start_dt, end_dt = _local_day_bounds(today_local)
|
||||
|
||||
data = _fetch_prices(start_dt, end_dt)
|
||||
envelope = PriceEnvelope(
|
||||
range=Range(
|
||||
from_utc=start_dt.astimezone(UTC).isoformat(),
|
||||
to_utc=end_dt.astimezone(UTC).isoformat(),
|
||||
),
|
||||
data=[PricePoint.model_validate(p) for p in data],
|
||||
)
|
||||
return JSONResponse(content=envelope.model_dump(by_alias=True))
|
||||
|
||||
|
||||
@app.get("/tomorrow", response_model=PriceEnvelope)
|
||||
def prices_tomorrow():
|
||||
tomorrow_local = (datetime.now(WARSAW_TZ) + timedelta(days=1)).date()
|
||||
start_dt, end_dt = _local_day_bounds(tomorrow_local)
|
||||
|
||||
data = _fetch_prices(start_dt, end_dt)
|
||||
envelope = PriceEnvelope(
|
||||
range=Range(
|
||||
from_utc=start_dt.astimezone(UTC).isoformat(),
|
||||
to_utc=end_dt.astimezone(UTC).isoformat(),
|
||||
),
|
||||
data=[PricePoint.model_validate(p) for p in data],
|
||||
)
|
||||
return JSONResponse(content=envelope.model_dump(by_alias=True))
|
||||
|
||||
|
||||
@app.get("/day/{day}", response_model=PriceEnvelope)
|
||||
def prices_for_day(
|
||||
day: str = Path(..., description="YYYY-MM-DD w lokalnej strefie Europe/Warsaw"),
|
||||
):
|
||||
try:
|
||||
y, m, d = map(int, day.split("-"))
|
||||
req_day = date(y, m, d)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="day must be in YYYY-MM-DD format")
|
||||
|
||||
start_dt, end_dt = _local_day_bounds(req_day)
|
||||
|
||||
data = _fetch_prices(start_dt, end_dt)
|
||||
envelope = PriceEnvelope(
|
||||
range=Range(
|
||||
from_utc=start_dt.astimezone(UTC).isoformat(),
|
||||
to_utc=end_dt.astimezone(UTC).isoformat(),
|
||||
),
|
||||
data=[PricePoint.model_validate(p) for p in data],
|
||||
)
|
||||
return JSONResponse(content=envelope.model_dump(by_alias=True))
|
||||
|
||||
import logging
|
||||
import time
|
||||
timelog = logging.getLogger("app.timing") # <= własny logger
|
||||
|
||||
@app.middleware("http")
|
||||
async def timing_middleware(request: Request, call_next):
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
response = await call_next(request)
|
||||
status = response.status_code
|
||||
except Exception:
|
||||
status = 500
|
||||
raise
|
||||
finally:
|
||||
dur_s = time.perf_counter() - start
|
||||
dur_ms = dur_s * 1000.0
|
||||
# Nagłówki dla klienta:
|
||||
# standard: Server-Timing (Chrome devtools ładnie pokazuje)
|
||||
response.headers["Server-Timing"] = f"app;dur={dur_ms:.2f}"
|
||||
# dodatkowy, „ludzki”:
|
||||
response.headers["X-Process-Time"] = f"{dur_s:.6f}s"
|
||||
# Log do stdout:
|
||||
timelog.info(f"{request.method} {request.url.path} -> {status} in {dur_ms} ms")
|
||||
return response
|
||||
# To run: uvicorn main:app --host 0.0.0.0 --port 8080
|
||||
Loading…
Reference in New Issue
Block a user