diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..8dee1c9 --- /dev/null +++ b/server/main.py @@ -0,0 +1,467 @@ +from fastapi import FastAPI, Depends, Query, Request, HTTPException +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 JSONResponse +import asyncio +import asyncpg +import redis +import json +from motor.motor_asyncio import AsyncIOMotorClient +import os + +print("TEST") + +redis_client = redis.StrictRedis(host='redis', port=6379, db=0, decode_responses=True) + +app = FastAPI() + +mongo_client = AsyncIOMotorClient("mongodb://mongo:27017") +mongo_db = mongo_client["mydatabase"] +mongo_collection = mongo_db["user_actions"] + +async def transfer_redis_to_mongo(): + while True: + entries = redis_client.lrange("user_actions", 0, -1) + if entries: + documents = [json.loads(e) for e in entries] + await mongo_collection.insert_many(documents) + redis_client.delete("user_actions") + print(f"Przeniesiono {len(documents)} wpisów z Redis do MongoDB") + else: + print("Brak nowych wpisów w Redis") + await asyncio.sleep(30) + +@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 + } + 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(): + asyncio.create_task(transfer_redis_to_mongo()) + 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.") + except Exception as e: + print(f"Próba {attempt+1}/50 nieudana: {e}") + await asyncio.sleep(2) + +@app.get("/weather", response_class=JSONResponse) +async def get_weather(city: str = Query("warszawa"), session: AsyncSession = Depends(get_session)): + coords = CITIES.get(city.lower()) + if not coords: + raise HTTPException(status_code=404, detail="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: + 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 z API pogodowym: {e}") + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=502, detail=f"Błąd API pogodowego: {e.response.status_code} - {e.response.text}") + + 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() + await session.refresh(record) + + return JSONResponse(content={ + "city": record.city, + "temperature": record.temperature, + "windspeed": record.windspeed, + "time": record.time.isoformat() + }) + +@app.get("/history", response_class=JSONResponse) +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 JSONResponse(content=[ + { + "city": r.city, + "temperature": r.temperature, + "windspeed": r.windspeed, + "time": r.time.isoformat() + } + for r in records + ]) + +@app.get("/view", response_class=JSONResponse) +async def view_history(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 JSONResponse(content={ + "city": city.capitalize(), + "records": [ + { + "city": r.city, + "temperature": r.temperature, + "windspeed": r.windspeed, + "time": r.time.isoformat() + } + for r in records + ] + }) + +@app.get("/ping", response_class=JSONResponse) +async def ping(): + return JSONResponse({"status": "ok"}) + +@app.get("/weather/geo", response_class=JSONResponse) +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() + await session.refresh(record) + + return JSONResponse(content={ + "city": record.city, + "latitude": latitude, + "longitude": longitude, + "temperature": record.temperature, + "windspeed": record.windspeed, + "time": record.time.isoformat() + }) + +@app.get("/forecast", response_class=JSONResponse) +async def get_forecast( + city: str = Query("warszawa"), + days: int = Query(3, ge=1, le=16) +): + coords = CITIES.get(city.lower()) + if not coords: + raise HTTPException(status_code=404, detail="Miasto nieobsługiwane") + + 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: + 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 z API pogodowym: {e}") + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=502, detail=f"Błąd API pogodowego: {e.response.status_code} - {e.response.text}") + + daily = data.get("daily", {}) + + return JSONResponse(content={ + "city": city.capitalize(), + "forecast": daily, + }) + +@app.get("/forecast/geo", response_class=JSONResponse) +async def get_forecast_geo( + 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: + 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 z API pogodowym: {e}") + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=502, detail=f"Błąd API pogodowego: {e.response.status_code} - {e.response.text}") + + daily = data.get("daily", {}) + + return JSONResponse(content={ + "latitude": latitude, + "longitude": longitude, + "forecast": daily + }) + +@app.get("/forecast/hourly/geo", response_class=JSONResponse) +async def get_hourly_forecast_geo( + 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: + 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 z API pogodowym: {e}") + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=502, detail=f"Błąd API pogodowego: {e.response.status_code} - {e.response.text}") + + hourly_data = {} + for key, values in data.get("hourly", {}).items(): + hourly_data[key] = values[:hours] + + return JSONResponse(content={ + "latitude": latitude, + "longitude": longitude, + "hourly_forecast": hourly_data + }) + +@app.get("/forecast/hourly", response_class=JSONResponse) +async def get_hourly_forecast_city( + city: str = Query(...), + hours: int = Query(12, ge=1, le=48) +): + coords = CITIES.get(city.lower()) + if not coords: + raise HTTPException(status_code=404, detail="Miasto nieobsługiwane") + 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: + 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 z API pogodowym: {e}") + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=502, detail=f"Błąd API pogodowego: {e.response.status_code} - {e.response.text}") + + hourly_data = {} + for key, values in data.get("hourly", {}).items(): + hourly_data[key] = values[:hours] + + return JSONResponse(content={ + "city": city.capitalize(), + "hourly_forecast": hourly_data + }) + +@app.get("/history-range", response_class=JSONResponse) +async def get_weather_history_range( + 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: + raise HTTPException(status_code=404, detail="Miasto nieobsługiwane") + + 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: + 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 z API pogodowym: {e}") + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=502, detail=f"Błąd API pogodowego: {e.response.status_code} - {e.response.text}") + + daily = data.get("daily", {}) + + return JSONResponse(content={ + "city": city.capitalize(), + "start_date": start_date, + "end_date": end_date, + "history": daily + }) + +@app.get("/history-range/geo", response_class=JSONResponse) +async def get_weather_history_range_geo( + 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: + 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 z API pogodowym: {e}") + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=502, detail=f"Błąd API pogodowego: {e.response.status_code} - {e.response.text}") + + daily = data.get("daily", {}) + + return JSONResponse(content={ + "latitude": latitude, + "longitude": longitude, + "start_date": start_date, + "end_date": end_date, + "history": daily + })