From 792f2092074c18ba99105003f01b06ab78ec1bba Mon Sep 17 00:00:00 2001 From: root Date: Thu, 17 Jul 2025 12:21:03 +0200 Subject: [PATCH] first commit --- db.py | 13 + main.py | 553 +++++++++++++++++++++++++++++++++ models.py | 12 + requirements.txt | 10 + templates/forecast.html | 69 ++++ templates/history.html | 57 ++++ templates/history_range.html | 67 ++++ templates/hourly_forecast.html | 67 ++++ templates/ping.html | 11 + templates/test.html | 28 ++ 10 files changed, 887 insertions(+) create mode 100644 db.py create mode 100644 main.py create mode 100644 models.py create mode 100644 requirements.txt create mode 100644 templates/forecast.html create mode 100644 templates/history.html create mode 100644 templates/history_range.html create mode 100644 templates/hourly_forecast.html create mode 100644 templates/ping.html create mode 100644 templates/test.html diff --git a/db.py b/db.py new file mode 100644 index 0000000..9821db3 --- /dev/null +++ b/db.py @@ -0,0 +1,13 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker, declarative_base + +DATABASE_URL = "postgresql+asyncpg://weather_user:weather_pass@localhost:5432/weather" + +engine = create_async_engine(DATABASE_URL, echo=False) +AsyncSessionLocal = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False) + +Base = declarative_base() + +async def get_session(): + async with AsyncSessionLocal() as session: + yield session diff --git a/main.py b/main.py new file mode 100644 index 0000000..46d6bf4 --- /dev/null +++ b/main.py @@ -0,0 +1,553 @@ +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 +from motor.motor_asyncio import AsyncIOMotorClient + + +redis_client = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True) + +app = FastAPI() +templates = Jinja2Templates(directory="templates") + +mongo_client = AsyncIOMotorClient("mongodb://localhost: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(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) +''' + +@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.") + #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 + }) + + diff --git a/models.py b/models.py new file mode 100644 index 0000000..e23a577 --- /dev/null +++ b/models.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime +from db import Base +from datetime import datetime + +class WeatherRecord(Base): + __tablename__ = "weather" + + id = Column(Integer, primary_key=True, index=True) + city = Column(String, index=True) + temperature = Column(Float) + windspeed = Column(Float) + time = Column(DateTime, default=datetime.utcnow) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d0cb937 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +fastapi +httpx +sqlalchemy +asyncpg +jinja2 +uvicorn +redis[asyncio] +python-multipart +aioredis +motor diff --git a/templates/forecast.html b/templates/forecast.html new file mode 100644 index 0000000..37630d0 --- /dev/null +++ b/templates/forecast.html @@ -0,0 +1,69 @@ + + + + + Prognoza pogody - {{ city }} + + + +

Prognoza pogody dla {{ city }}

+ + {% if error %} +

{{ error }}

+ {% elif forecast and forecast.time %} + + + + + + + + + + + {% for i in range(forecast.time|length) %} + + + + + + + {% endfor %} + +
DataTemperatura maks. (°C)Temperatura min. (°C)Opady (mm)
{{ forecast.time[i] }}{{ forecast.temperature_2m_max[i] }}{{ forecast.temperature_2m_min[i] }}{{ forecast.precipitation_sum[i] }}
+ {% else %} +

Brak danych pogodowych.

+ {% endif %} + + diff --git a/templates/history.html b/templates/history.html new file mode 100644 index 0000000..2a1710a --- /dev/null +++ b/templates/history.html @@ -0,0 +1,57 @@ + + + + + Historia zapytan o {{ city }} + + + +

Historia zapytan o {{ city }}

+ + + + + + + + {% for r in records %} + + + + + + {% endfor %} +
DataTemperatura (°C)Wiatr (m/s)
{{ r.time.strftime('%Y-%m-%d %H:%M') }}{{ r.temperature }}{{ r.windspeed }}
+ + diff --git a/templates/history_range.html b/templates/history_range.html new file mode 100644 index 0000000..3754067 --- /dev/null +++ b/templates/history_range.html @@ -0,0 +1,67 @@ + + + + + Historia pogody - {{ city }} + + + +

Historia pogody dla {{ city }}
od {{ start_date }} do {{ end_date }}

+ + {% if error %} +

{{ error }}

+ {% elif history %} + + + + + + + + + + + {% for i in range(history["time"]|length) %} + + + + + + + {% endfor %} + +
DataTemp. maks. (°C)Temp. min. (°C)Opady (mm)
{{ history["time"][i] }}{{ history["temperature_2m_max"][i] }}{{ history["temperature_2m_min"][i] }}{{ history["precipitation_sum"][i] }}
+ {% endif %} + + diff --git a/templates/hourly_forecast.html b/templates/hourly_forecast.html new file mode 100644 index 0000000..69029c6 --- /dev/null +++ b/templates/hourly_forecast.html @@ -0,0 +1,67 @@ + + + + + Prognoza godzinowa - {{ city }} + + + +

Prognoza godzinowa dla {{ city }}

+ + {% if error %} +

{{ error }}

+ {% elif hourly_forecast %} + + + + + + + + + + + {% for i in range(hourly_forecast["time"]|length) %} + + + + + + + {% endfor %} + +
GodzinaTemp. (°C)Wiatr (km/h)Opady (mm)
{{ hourly_forecast["time"][i] }}{{ hourly_forecast["temperature_2m"][i] }}{{ hourly_forecast["windspeed_10m"][i] }}{{ hourly_forecast["precipitation"][i] }}
+ {% endif %} + + diff --git a/templates/ping.html b/templates/ping.html new file mode 100644 index 0000000..d5de17b --- /dev/null +++ b/templates/ping.html @@ -0,0 +1,11 @@ + + + + + Ping status + + +

Status: OK

+

Serwer działa poprawnie.

+ + diff --git a/templates/test.html b/templates/test.html new file mode 100644 index 0000000..99643a7 --- /dev/null +++ b/templates/test.html @@ -0,0 +1,28 @@ + + + + Historia pogody - {{ city }} + + + +

Historia pogody – {{ city }}

+ + + + + + + {% for r in records %} + + + + + + {% endfor %} +
DataTemperatura (°C)Wiatr (m/s)
{{ r.time.strftime('%Y-%m-%d %H:%M') }}{{ r.temperature }}{{ r.windspeed }}
+ +