from fastapi import FastAPI, Depends, Query, Request from fastapi.templating import Jinja2Templates import httpx from sqlalchemy.future import select from sqlalchemy.ext.asyncio import AsyncSession from db import get_session, engine, Base from models import WeatherRecord from datetime import datetime from fastapi.responses import HTMLResponse, JSONResponse #from tenacity import retry, stop_after_attempt, wait_fixed #DODANE import asyncio import asyncpg import redis import json redis_client = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True) app = FastAPI() templates = Jinja2Templates(directory="templates") @app.middleware("http") async def log_user_action(request: Request, call_next): method = request.method path = request.url.path query = dict(request.query_params) timestamp = datetime.utcnow().isoformat() action = { "method": method, "path": path, "query": query, "timestamp": timestamp } # Zapisujemy jako JSON w liście w Redis redis_client.rpush("user_actions", json.dumps(action)) response = await call_next(request) return response @app.get("/logs") def get_logs(): entries = redis_client.lrange("user_actions", 0, -1) return [json.loads(e) for e in entries] CITIES = { "warszawa": (52.23, 21.01), "krakow": (50.06, 19.94), "gdansk": (54.35, 18.65), "wroclaw": (51.11, 17.03), "poznan": (52.41, 16.93), "szczecin": (53.43, 14.55), "bydgoszcz": (53.12, 18.01), "lublin": (51.25, 22.57), "bialystok": (53.13, 23.15), "katowice": (50.26, 19.02), "lodz": (51.77, 19.46), "torun": (53.01, 18.60), "kielce": (50.87, 20.63), "rzeszow": (50.04, 22.00), "opole": (50.67, 17.93), "zielona_gora": (51.94, 15.50), "gorzow_wlkp": (52.73, 15.24), "olsztyn": (53.78, 20.48), "radom": (51.40, 21.15), "plock": (52.55, 19.70), "elblag": (54.16, 19.40), "tarnow": (50.01, 20.99), "chorzow": (50.30, 18.95), "gliwice": (50.30, 18.67), "zabrze": (50.30, 18.78), "rybnik": (50.10, 18.55), "walbrzych": (50.77, 16.28), "legnica": (51.21, 16.16), "pila": (53.15, 16.74), "suwalki": (54.10, 22.93), "siedlce": (52.17, 22.29), "piotrkow_tryb": (51.40, 19.70), "nowy_sacz": (49.62, 20.69), "przemysl": (49.78, 22.77), "zamosc": (50.72, 23.25), "chelm": (51.14, 23.47), "koszalin": (54.19, 16.18), "slupsk": (54.46, 17.03), "grudziadz": (53.48, 18.75), "jaworzno": (50.20, 19.27), "tarnobrzeg": (50.58, 21.68), "ostrow_wlkp": (51.65, 17.81), "konin": (52.22, 18.26), "leszno": (51.84, 16.57), "stargard": (53.34, 15.05), "lubin": (51.40, 16.20), "mielec": (50.28, 21.42), "pabianice": (51.66, 19.35), "glogow": (51.66, 16.08), "ostroleka": (53.09, 21.56), "siemianowice_sl": (50.31, 19.03), "swidnica": (50.84, 16.49), "skierniewice": (51.97, 20.15), "bedzin": (50.33, 19.13), "pulawy": (51.42, 21.97), "starachowice": (51.05, 21.08), "nowy_targ": (49.48, 20.03), "radomsko": (51.07, 19.45), "wloclawek": (52.65, 19.07), "lubartow": (51.46, 22.61), "lomza": (53.18, 22.07), "klodzko": (50.43, 16.65), "biala_podlaska": (52.03, 23.13), "pruszkow": (52.17, 20.80), "jaslo": (49.75, 21.47), "dzierzoniow": (50.73, 16.65), "bielsk_podlaski": (52.77, 23.19), } ''' @app.on_event("startup") async def startup(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) ''' @app.on_event("startup") async def startup(): for attempt in range(50): try: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) print("Połączono z bazą danych.") break except asyncpg.InvalidPasswordError: print("Nieprawidłowe hasło.") #raise except Exception as e: #print(f"Próba {attempt+1}/10 nieudana: {e}") await asyncio.sleep(2) @app.get("/weather") async def get_weather(city: str = Query("warszawa"), session: AsyncSession = Depends(get_session)): coords = CITIES.get(city.lower()) if not coords: return {"error": "Miasto nieobsługiwane"} url = f"http://api.open-meteo.com/v1/forecast?latitude={coords[0]}&longitude={coords[1]}¤t_weather=true" async with httpx.AsyncClient() as client: res = await client.get(url) data = res.json() current = data.get("current_weather", {}) record = WeatherRecord( city=city.capitalize(), temperature=current.get("temperature"), windspeed=current.get("windspeed"), time=datetime.fromisoformat(current.get("time")), ) session.add(record) await session.commit() return { "city": record.city, "temperature": record.temperature, "windspeed": record.windspeed, "time": record.time } @app.get("/history") async def get_history(city: str = Query("warszawa"), session: AsyncSession = Depends(get_session)): stmt = select(WeatherRecord).where(WeatherRecord.city.ilike(city)).order_by(WeatherRecord.time.desc()).limit(10) result = await session.execute(stmt) records = result.scalars().all() return [ { "city": r.city, "temperature": r.temperature, "windspeed": r.windspeed, "time": r.time.isoformat() } for r in records ] @app.get("/view") async def view_history(request: Request, city: str = "warszawa", session: AsyncSession = Depends(get_session)): stmt = select(WeatherRecord).where(WeatherRecord.city.ilike(city)).order_by(WeatherRecord.time.desc()).limit(10) result = await session.execute(stmt) records = result.scalars().all() return templates.TemplateResponse("history.html", { "request": request, "city": city.capitalize(), "records": records }) @app.get("/ping") async def ping(request: Request): if "text/html" in request.headers.get("accept", ""): return templates.TemplateResponse("ping.html", {"request": request}) return JSONResponse({"status": "ok"}) @app.get("/weather/geo") async def get_weather_by_coords( latitude: float = Query(..., ge=-90, le=90), longitude: float = Query(..., ge=-180, le=180), session: AsyncSession = Depends(get_session) ): url = ( f"http://api.open-meteo.com/v1/forecast?" f"latitude={latitude}&longitude={longitude}¤t_weather=true" ) async with httpx.AsyncClient() as client: try: res = await client.get(url, timeout=10.0) res.raise_for_status() data = res.json() except httpx.RequestError as e: raise HTTPException(status_code=502, detail=f"Błąd połączenia: {e}") except httpx.HTTPStatusError as e: raise HTTPException(status_code=502, detail=f"Błąd API pogodowego: {e}") current = data.get("current_weather", {}) record = WeatherRecord( city="Custom Location", temperature=current.get("temperature"), windspeed=current.get("windspeed"), time=datetime.fromisoformat(current.get("time")), ) session.add(record) await session.commit() return { "city": record.city, "latitude": latitude, "longitude": longitude, "temperature": record.temperature, "windspeed": record.windspeed, "time": record.time } @app.get("/forecast", response_class=HTMLResponse) async def get_forecast( request: Request, city: str = Query("warszawa"), days: int = Query(3, ge=1, le=16) ): coords = CITIES.get(city.lower()) if not coords: return templates.TemplateResponse( "forecast.html", { "request": request, "city": city.capitalize(), "forecast": None } ) url = ( f"http://api.open-meteo.com/v1/forecast?" f"latitude={coords[0]}&longitude={coords[1]}" f"&daily=temperature_2m_max,temperature_2m_min,precipitation_sum" f"&timezone=Europe/Warsaw" f"&forecast_days={days}" ) async with httpx.AsyncClient() as client: res = await client.get(url) data = res.json() daily = data.get("daily", {}) accept = request.headers.get("accept", "") if "text/html" in accept: return templates.TemplateResponse( "forecast.html", { "request": request, "city": city.capitalize(), "forecast": daily, "error": None, } ) else: return JSONResponse(content={ "city": city.capitalize(), "forecast": daily, }) @app.get("/forecast/geo", response_class=HTMLResponse) async def get_forecast_geo( request: Request, latitude: float = Query(..., ge=-90, le=90), longitude: float = Query(..., ge=-180, le=180), days: int = Query(3, ge=1, le=16) ): url = ( f"http://api.open-meteo.com/v1/forecast?" f"latitude={latitude}&longitude={longitude}" f"&daily=temperature_2m_max,temperature_2m_min,precipitation_sum" f"&timezone=Europe/Warsaw" f"&forecast_days={days}" ) async with httpx.AsyncClient() as client: res = await client.get(url) data = res.json() daily = data.get("daily", {}) accept = request.headers.get("accept", "") if "text/html" in accept: return templates.TemplateResponse( "forecast.html", { "request": request, "city": f"Lat: {latitude}, Lon: {longitude}", "forecast": daily, "error": None, } ) else: return JSONResponse(content={ "latitude": latitude, "longitude": longitude, "forecast": daily }) @app.get("/forecast/hourly/geo", response_class=HTMLResponse) async def get_hourly_forecast_geo( request: Request, latitude: float = Query(..., ge=-90, le=90), longitude: float = Query(..., ge=-180, le=180), hours: int = Query(12, ge=1, le=48) ): url = ( f"http://api.open-meteo.com/v1/forecast?" f"latitude={latitude}&longitude={longitude}" f"&hourly=temperature_2m,windspeed_10m,precipitation" f"&timezone=Europe/Warsaw" ) async with httpx.AsyncClient() as client: res = await client.get(url) data = res.json() hourly_data = {} for key, values in data.get("hourly", {}).items(): hourly_data[key] = values[:hours] accept = request.headers.get("accept", "") if "text/html" in accept: return templates.TemplateResponse( "hourly_forecast.html", { "request": request, "city": f"Lat: {latitude}, Lon: {longitude}", "hourly_forecast": hourly_data, "error": None, } ) else: return JSONResponse(content={ "latitude": latitude, "longitude": longitude, "hourly_forecast": hourly_data }) @app.get("/forecast/hourly") async def get_hourly_forecast_city( request: Request, city: str = Query(...), hours: int = Query(12, ge=1, le=48) ): coords = CITIES.get(city.lower()) if not coords: error_data = {"error": "Miasto nieobsługiwane"} accept = request.headers.get("accept", "") if "text/html" in accept: return templates.TemplateResponse( "hourly_forecast.html", { "request": request, "city": city.capitalize(), "hourly_forecast": None, "error": error_data["error"], } ) else: raise HTTPException(status_code=404, detail=error_data["error"]) latitude, longitude = coords url = ( f"http://api.open-meteo.com/v1/forecast?" f"latitude={latitude}&longitude={longitude}" f"&hourly=temperature_2m,windspeed_10m,precipitation" f"&timezone=Europe/Warsaw" ) async with httpx.AsyncClient() as client: res = await client.get(url) data = res.json() hourly_data = {} for key, values in data.get("hourly", {}).items(): hourly_data[key] = values[:hours] accept = request.headers.get("accept", "") if "text/html" in accept: return templates.TemplateResponse( "hourly_forecast.html", { "request": request, "city": city.capitalize(), "hourly_forecast": hourly_data, "error": None, } ) else: return JSONResponse(content={ "city": city.capitalize(), "hourly_forecast": hourly_data }) @app.get("/history-range") async def get_weather_history_range( request: Request, city: str = Query(..., description="Nazwa miasta, np. warszawa"), start_date: str = Query(..., description="Początkowa data w formacie YYYY-MM-DD"), end_date: str = Query(..., description="Końcowa data w formacie YYYY-MM-DD") ): coords = CITIES.get(city.lower()) if not coords: error_data = {"error": "Miasto nieobsługiwane"} accept = request.headers.get("accept", "") if "text/html" in accept: return templates.TemplateResponse( "history_range.html", { "request": request, "city": city.capitalize(), "start_date": start_date, "end_date": end_date, "history": None, "error": error_data["error"] } ) else: raise HTTPException(status_code=404, detail=error_data["error"]) latitude, longitude = coords url = ( f"http://archive-api.open-meteo.com/v1/archive?" f"latitude={latitude}&longitude={longitude}" f"&daily=temperature_2m_max,temperature_2m_min,precipitation_sum" f"&start_date={start_date}&end_date={end_date}" f"&timezone=Europe/Warsaw" ) async with httpx.AsyncClient() as client: res = await client.get(url) data = res.json() daily = data.get("daily", {}) accept = request.headers.get("accept", "") if "text/html" in accept: return templates.TemplateResponse( "history_range.html", { "request": request, "city": city.capitalize(), "start_date": start_date, "end_date": end_date, "history": daily, "error": None } ) else: return JSONResponse(content={ "city": city.capitalize(), "start_date": start_date, "end_date": end_date, "history": daily }) @app.get("/history-range/geo") async def get_weather_history_range_geo( request: Request, latitude: float = Query(..., ge=-90, le=90, description="Szerokość geograficzna"), longitude: float = Query(..., ge=-180, le=180, description="Długość geograficzna"), start_date: str = Query(..., description="Początkowa data w formacie YYYY-MM-DD"), end_date: str = Query(..., description="Końcowa data w formacie YYYY-MM-DD") ): url = ( f"http://archive-api.open-meteo.com/v1/archive?" f"latitude={latitude}&longitude={longitude}" f"&daily=temperature_2m_max,temperature_2m_min,precipitation_sum" f"&start_date={start_date}&end_date={end_date}" f"&timezone=Europe/Warsaw" ) async with httpx.AsyncClient() as client: res = await client.get(url) data = res.json() daily = data.get("daily", {}) accept = request.headers.get("accept", "") if "text/html" in accept: city_name = f"({latitude}, {longitude})" return templates.TemplateResponse( "history_range.html", { "request": request, "city": city_name, "start_date": start_date, "end_date": end_date, "history": daily, "error": None } ) else: return JSONResponse(content={ "latitude": latitude, "longitude": longitude, "start_date": start_date, "end_date": end_date, "history": daily })