small fixes
This commit is contained in:
parent
39d28d2e8d
commit
db9269f056
@ -69,7 +69,6 @@ def get_month():
|
|||||||
start = start + timedelta(days=1)
|
start = start + timedelta(days=1)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TGE_RDNScraper(EnergyPriceScraperBase, HasLogger):
|
class TGE_RDNScraper(EnergyPriceScraperBase, HasLogger):
|
||||||
"""
|
"""
|
||||||
Scraper TGE RDN 'Notowania w systemie kursu jednolitego / fixing' z pliku Excel.
|
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"
|
sudo -u energy "$APP_DIR/.venv/bin/pip" install -r "$APP_DIR/requirements.txt"
|
||||||
fi
|
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
|
sudo systemctl restart energy-price-scrapers.service
|
||||||
|
|
||||||
echo "Updated & restarted."
|
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