feat(api): add multiple days api

This commit is contained in:
2024-07-29 21:42:44 +03:00
parent 22fab7a15a
commit 7f0e19fb5a
16 changed files with 5411 additions and 64 deletions

View File

@@ -9,15 +9,24 @@ from weather.model import WeatherResponse, WeatherValue
from . import datehelp from . import datehelp
from .location import LOCATION_BUNDLE from .location import LOCATION_BUNDLE
from .parser import LOCATION_PARSER, ONE_DAY_PARSER, ROW_PARSERS from .parser import DAYS_PARSER, LOCATION_PARSER, ONE_DAY_PARSER, ROW_PARSERS
class GismeteoApi(WeatherApi): class GismeteoApi(WeatherApi):
BASE_URL = "https://www.gismeteo.ru" BASE_URL = "https://www.gismeteo.ru"
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
COOKIE = "cf_clearance=U28mYVC0ENu88vorlL_CWmWOoevvXp0vb4xCqfqYC9s-1722273367-1.0.1.1-IDV73azTHY0V.NAnmEvok3zf5HHEkvF098pmya7IiqRRB5nk3FhbLCb0AeWm_kpTFqi1niFk2mYN_ramGTSl0A"
async def _request(self, endpoint: str) -> str: async def _request(self, endpoint: str) -> str:
url = f"{self.BASE_URL}/{endpoint}" url = f"{self.BASE_URL}/{endpoint}"
async with aiohttp.ClientSession(raise_for_status=True) as session: async with aiohttp.ClientSession(
headers={
"User-Agent": self.USER_AGENT,
"Cookie": self.COOKIE,
},
raise_for_status=True,
) as session:
async with session.request("GET", url) as response: async with session.request("GET", url) as response:
return await response.text() return await response.text()
@@ -39,7 +48,31 @@ class GismeteoApi(WeatherApi):
values=values, values=values,
) )
def _parse_manydays(self, data: str) -> WeatherResponse:
result: List[Dict[str, Any]] = []
soup = BeautifulSoup(data, features="html.parser")
location = LOCATION_PARSER.parse_location(data)
widget = DAYS_PARSER.parse_widget(soup)
for parser in ROW_PARSERS:
for index, value in enumerate(parser.parse_row(widget)):
while len(result) < index + 1:
result.append({})
result[index][parser.KEY] = value
print(">", result)
values = [WeatherValue(**item) for item in result]
return WeatherResponse(
location=location or "n/a",
date=datetime.date.today(),
period="days",
values=values,
)
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse: async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
location = LOCATION_BUNDLE.parse(location_id) location = LOCATION_BUNDLE.parse(location_id)
data = await self._request(f"weather-{location}/{datehelp.dump(date)}") data = await self._request(f"weather-{location}/{datehelp.dump(date)}")
return self._parse_oneday(date, data) return self._parse_oneday(date, data)
async def get_days(self, location_id: str, days: int) -> WeatherResponse:
location = LOCATION_BUNDLE.parse(location_id)
data = await self._request(f"weather-{location}/{days}-days")
return self._parse_manydays(data)

View File

@@ -6,13 +6,11 @@ from gismeteo.api import WeatherResponse
class MockData: class MockData:
@property def get_html(self, key: str) -> str:
def html(self) -> str: return (Path(__file__).parent / f"data/{key}.html").read_text()
return (Path(__file__).parent / "data/weather.html").read_text()
@property def get_response(self, key: str) -> WeatherResponse:
def response(self) -> WeatherResponse: data = json.loads((Path(__file__).parent / f"data/{key}.json").read_text())
data = json.loads((Path(__file__).parent / "data/weather.json").read_text())
return WeatherResponse(**data) return WeatherResponse(**data)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"location":"Орел","date":"2024-07-29","period":"day","values":[{"date":"2024-07-29T00:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[20],"wind_speed":1,"wind_gust":1,"wind_direction":"SW","precipitation":0.0,"pressure":[744],"humidity":85},{"date":"2024-07-29T03:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[18],"wind_speed":1,"wind_gust":1,"wind_direction":"W","precipitation":0.6,"pressure":[742],"humidity":96},{"date":"2024-07-29T06:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":false,"fog":false},"temperature":[19],"wind_speed":1,"wind_gust":2,"wind_direction":"S","precipitation":4.9,"pressure":[741],"humidity":95},{"date":"2024-07-29T09:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":false,"fog":false},"temperature":[19],"wind_speed":3,"wind_gust":7,"wind_direction":"S","precipitation":3.8,"pressure":[740],"humidity":83},{"date":"2024-07-29T12:00:00","sky":{"cloudness":"clear","precipitation":"no","thunder":false,"fog":false},"temperature":[21],"wind_speed":4,"wind_gust":11,"wind_direction":"W","precipitation":0.0,"pressure":[740],"humidity":54},{"date":"2024-07-29T15:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[21],"wind_speed":4,"wind_gust":10,"wind_direction":"SW","precipitation":0.0,"pressure":[738],"humidity":48},{"date":"2024-07-29T18:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[19],"wind_speed":3,"wind_gust":10,"wind_direction":"SW","precipitation":0.0,"pressure":[737],"humidity":63},{"date":"2024-07-29T21:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[17],"wind_speed":3,"wind_gust":7,"wind_direction":"SW","precipitation":0.0,"pressure":[737],"humidity":77}]}

View File

@@ -0,0 +1 @@
{"location":"Орел","date":"2024-07-29","period":"days","values":[{"date":"2024-07-29T00:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":false,"fog":false},"temperature":[21,17],"wind_speed":4,"wind_gust":11,"wind_direction":"W","precipitation":9.3,"pressure":[744,737],"humidity":96},{"date":"2024-07-30T00:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":true,"fog":false},"temperature":[19,14],"wind_speed":2,"wind_gust":7,"wind_direction":"N","precipitation":11.0,"pressure":[737,733],"humidity":100},{"date":"2024-07-31T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[22,14],"wind_speed":3,"wind_gust":10,"wind_direction":"NW","precipitation":1.8,"pressure":[741,738],"humidity":99},{"date":"2024-07-01T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[24,14],"wind_speed":3,"wind_gust":10,"wind_direction":"W","precipitation":0.1,"pressure":[741,740],"humidity":97},{"date":"2024-07-02T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[24,17],"wind_speed":2,"wind_gust":8,"wind_direction":"W","precipitation":0.2,"pressure":[740],"humidity":84},{"date":"2024-07-03T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[25,14],"wind_speed":1,"wind_gust":4,"wind_direction":"N","precipitation":0.0,"pressure":[740,739],"humidity":99},{"date":"2024-07-04T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[25,14],"wind_speed":3,"wind_gust":6,"wind_direction":"N","precipitation":0.0,"pressure":[743,740],"humidity":92},{"date":"2024-07-05T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":true,"fog":false},"temperature":[25,15],"wind_speed":3,"wind_gust":7,"wind_direction":"NW","precipitation":2.1,"pressure":[744,743],"humidity":98},{"date":"2024-07-06T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[24,14],"wind_speed":3,"wind_gust":5,"wind_direction":"NW","precipitation":0.3,"pressure":[745,744],"humidity":98},{"date":"2024-07-07T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[26,14],"wind_speed":2,"wind_gust":5,"wind_direction":"NW","precipitation":0.2,"pressure":[747,745],"humidity":95}]}

View File

@@ -1 +0,0 @@
{"location":"Орел","date":"2024-07-29","period":"day","values":[{"date":"2024-07-29T00:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":20,"wind_speed":1,"wind_gust":1,"wind_direction":"SW","precipitation":0.0,"pressure":744,"humidity":85},{"date":"2024-07-29T03:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":19,"wind_speed":1,"wind_gust":1,"wind_direction":"W","precipitation":0.6,"pressure":743,"humidity":97},{"date":"2024-07-29T06:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":false,"fog":false},"temperature":19,"wind_speed":1,"wind_gust":3,"wind_direction":"S","precipitation":3.3,"pressure":741,"humidity":95},{"date":"2024-07-29T09:00:00","sky":{"cloudness":"cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":21,"wind_speed":3,"wind_gust":10,"wind_direction":"SW","precipitation":0.3,"pressure":740,"humidity":80},{"date":"2024-07-29T12:00:00","sky":{"cloudness":"party_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":21,"wind_speed":4,"wind_gust":12,"wind_direction":"W","precipitation":0.0,"pressure":740,"humidity":60},{"date":"2024-07-29T15:00:00","sky":{"cloudness":"cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":22,"wind_speed":4,"wind_gust":11,"wind_direction":"SW","precipitation":0.0,"pressure":739,"humidity":50},{"date":"2024-07-29T18:00:00","sky":{"cloudness":"cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":20,"wind_speed":4,"wind_gust":11,"wind_direction":"SW","precipitation":0.0,"pressure":738,"humidity":57},{"date":"2024-07-29T21:00:00","sky":{"cloudness":"cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":17,"wind_speed":2,"wind_gust":8,"wind_direction":"SW","precipitation":0.0,"pressure":737,"humidity":74}]}

View File

@@ -1,6 +1,6 @@
import datetime import datetime
import re import re
from typing import Dict, Iterable, List, Optional from typing import Iterable
import dateparser import dateparser
from bs4 import Tag from bs4 import Tag
@@ -10,12 +10,13 @@ from weather.model import Cloudness, Precipitation, Sky, WindDirection
from .core import BaseWidgetParser, RowParser from .core import BaseWidgetParser, RowParser
ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items") ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items")
DAYS_PARSER = BaseWidgetParser(".widget.widget-days .widget-items")
class LocationParser: class LocationParser:
PATTERN = re.compile('{"ru":{"city":{"name":"(.*?)"') PATTERN = re.compile('{"ru":{"city":{"name":"(.*?)"')
def parse_location(self, data: str) -> Optional[str]: def parse_location(self, data: str) -> str | None:
match = self.PATTERN.search(data) match = self.PATTERN.search(data)
if match: if match:
return match.group(1) return match.group(1)
@@ -29,30 +30,35 @@ class DateParser(RowParser[datetime.datetime]):
KEY = "date" KEY = "date"
def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]: def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]:
date_str = ( datetime_date_tag = tag.select_one(
tag.select_one(".widget-row.widget-row-datetime-date > .row-item") ".widget-row.widget-row-datetime-date > .row-item"
.find(text=True, recursive=False)
.text
) )
date = dateparser.parse(date_str, languages=["ru"]) if datetime_date_tag:
for item in tag.select(".widget-row.widget-row-datetime-time > .row-item"): date_str = datetime_date_tag.find(text=True, recursive=False).text
time_str = item.text date = dateparser.parse(date_str, languages=["ru"])
time = dateparser.parse(time_str, languages=["ru"]) for item in tag.select(".widget-row.widget-row-datetime-time > .row-item"):
time = time.replace(year=date.year, month=date.month, day=date.day) time_str = item.text
yield time time = dateparser.parse(time_str, languages=["ru"])
time = time.replace(year=date.year, month=date.month, day=date.day)
yield time
else:
for item in tag.select(".widget-row.widget-row-date > .row-item"):
date_str = item.text
date = dateparser.parse(date_str, languages=["ru"])
yield date
class SkyParser(RowParser[Sky]): class SkyParser(RowParser[Sky]):
KEY = "sky" KEY = "sky"
CLOUDNESS_MAP: Dict[str, Cloudness] = { CLOUDNESS_MAP: dict[str, Cloudness] = {
"ясно": Cloudness.CLEAR, "ясно": Cloudness.CLEAR,
"малооблачно": Cloudness.PARTLY_CLOUDY, "малооблачно": Cloudness.PARTLY_CLOUDY,
"облачно": Cloudness.CLOUDY, "облачно": Cloudness.CLOUDY,
"пасмурно": Cloudness.MAINLY_CLOUDY, "пасмурно": Cloudness.MAINLY_CLOUDY,
} }
PRECIPITATION_MAP: Dict[str, Precipitation] = { PRECIPITATION_MAP: dict[str, Precipitation] = {
"без осадков": Precipitation.NO, "без осадков": Precipitation.NO,
"небольшой дождь": Precipitation.SMALL_RAIN, "небольшой дождь": Precipitation.SMALL_RAIN,
"дождь": Precipitation.RAIN, "дождь": Precipitation.RAIN,
@@ -83,14 +89,16 @@ class SkyParser(RowParser[Sky]):
) )
class TemperatureParser(RowParser[int]): class TemperatureParser(RowParser[list[int]]):
KEY = "temperature" KEY = "temperature"
def parse_row(self, tag: Tag) -> Iterable[int]: def parse_row(self, tag: Tag) -> Iterable[list[int]]:
for item in tag.select( for item in tag.select(
".widget-row-chart[data-row=temperature-air] > .chart > .values > .value > temperature-value" ".widget-row-chart[data-row=temperature-air] > .chart > .values > .value"
): ):
yield int(item.attrs["value"]) yield [
int(value.attrs["value"]) for value in item.select("temperature-value")
]
class WindSpeedParser(RowParser[int]): class WindSpeedParser(RowParser[int]):
@@ -115,7 +123,7 @@ class WindGustParser(RowParser[int]):
class WindDirectionParser(RowParser[WindDirection]): class WindDirectionParser(RowParser[WindDirection]):
KEY = "wind_direction" KEY = "wind_direction"
WIND_DIRECTION_MAP: Dict[str, WindDirection] = { WIND_DIRECTION_MAP: dict[str, WindDirection] = {
"штиль": WindDirection.CALM, "штиль": WindDirection.CALM,
"с": WindDirection.N, "с": WindDirection.N,
"св": WindDirection.NO, "св": WindDirection.NO,
@@ -145,14 +153,14 @@ class WindPrecipitationParser(RowParser[float]):
yield float(item.text.replace(",", ".")) yield float(item.text.replace(",", "."))
class PressureParser(RowParser[int]): class PressureParser(RowParser[list[int]]):
KEY = "pressure" KEY = "pressure"
def parse_row(self, tag: Tag) -> Iterable[int]: def parse_row(self, tag: Tag) -> Iterable[list[int]]:
for item in tag.select( for item in tag.select(
".widget-row-chart[data-row=pressure] > .chart > .values > .value > pressure-value" ".widget-row-chart[data-row=pressure] > .chart > .values > .value"
): ):
yield int(item.attrs["value"]) yield [int(value.attrs["value"]) for value in item.select("pressure-value")]
class HumidityParser(RowParser[int]): class HumidityParser(RowParser[int]):
@@ -163,7 +171,7 @@ class HumidityParser(RowParser[int]):
yield int(item.text) yield int(item.text)
ROW_PARSERS: List[RowParser] = [ ROW_PARSERS: list[RowParser] = [
DateParser(), DateParser(),
SkyParser(), SkyParser(),
TemperatureParser(), TemperatureParser(),
@@ -175,4 +183,4 @@ ROW_PARSERS: List[RowParser] = [
HumidityParser(), HumidityParser(),
] ]
ROW_PARSERS_MAP: Dict[str, RowParser] = {parser.KEY: parser for parser in ROW_PARSERS} ROW_PARSERS_MAP: dict[str, RowParser] = {parser.KEY: parser for parser in ROW_PARSERS}

View File

@@ -11,14 +11,17 @@ def gismeteo_api_fixture() -> GismeteoApi:
api = GismeteoApi() api = GismeteoApi()
async def _request(endpoint: str) -> str: async def _request(endpoint: str) -> str:
return MOCK_DATA.html return MOCK_DATA.get_html(endpoint.split("/")[-1])
api._request = _request api._request = _request
return api return api
async def test_api(gismeteo_api: GismeteoApi): async def test_day(gismeteo_api: GismeteoApi):
result = await gismeteo_api.get_day( result = await gismeteo_api.get_day("zmiyevka", datetime.date.today())
"zmiyevka", datetime.date.today() + datetime.timedelta(days=1)
)
assert len(result.values) == 8 assert len(result.values) == 8
async def test_days(gismeteo_api: GismeteoApi):
result = await gismeteo_api.get_days("zmiyevka", 10)
assert len(result.values) == 10

View File

@@ -6,3 +6,6 @@ from .model import WeatherResponse
class WeatherApi: class WeatherApi:
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse: async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
raise NotImplementedError raise NotImplementedError
async def get_days(self, location_id: str, days: int) -> WeatherResponse:
raise NotImplementedError

View File

@@ -9,7 +9,6 @@ from .route import api, doc, view
def build_app(weather_api: WeatherApi) -> FastAPI: def build_app(weather_api: WeatherApi) -> FastAPI:
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
app = FastAPI( app = FastAPI(
title="Weather", title="Weather",
docs_url=None, docs_url=None,

View File

@@ -2,12 +2,21 @@ import datetime
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from weather.api import WeatherApi
from weather.model import WeatherResponse from weather.model import WeatherResponse
def mount(app: FastAPI): def mount(app: FastAPI):
@app.get("/api/weather/{location}/{date}") @app.get("/api/weather/{location}/day/{date}")
async def get_weather_api( async def get_api_weather_day(
request: Request, location: str, date: datetime.date request: Request, location: str, date: datetime.date
) -> WeatherResponse: ) -> WeatherResponse:
return await request.app.state.weather_api.get_day(location, date) weather_api: WeatherApi = request.app.state.weather_api
return await weather_api.get_day(location, date)
@app.get("/api/weather/{location}/days/{days}")
async def get_api_weather_days(
request: Request, location: str, days: int
) -> WeatherResponse:
weather_api: WeatherApi = request.app.state.weather_api
return await weather_api.get_days(location, days)

View File

@@ -9,6 +9,7 @@ from fastapi.templating import Jinja2Templates
from gismeteo import datehelp from gismeteo import datehelp
from gismeteo.location import LOCATION_BUNDLE from gismeteo.location import LOCATION_BUNDLE
from gismeteo.mock import MOCK_DATA from gismeteo.mock import MOCK_DATA
from weather.api import WeatherApi
from weather.model import WeatherResponse from weather.model import WeatherResponse
from .filters import cloudness_icon, wind_direction_icon from .filters import cloudness_icon, wind_direction_icon
@@ -47,20 +48,26 @@ def mount(app: FastAPI):
@app.get("/weather/{location}", response_class=RedirectResponse) @app.get("/weather/{location}", response_class=RedirectResponse)
async def get_weather_default(location: str): async def get_weather_default(location: str):
return RedirectResponse(f"{location}/{datetime.date.today()}") return RedirectResponse(f"{location}/day/{datetime.date.today()}")
@app.get("/weather/{location}/mock", response_class=HTMLResponse) @app.get("/weather/{location}/day/mock", response_class=HTMLResponse)
async def get_weather_mock(request: Request): async def get_weather_day_mock(request: Request):
response = MOCK_DATA.response response = MOCK_DATA.get_response("day")
return build_weather_response(request, response) return build_weather_response(request, response)
# @app.get("/weather/{location}/{day}", response_class=HTMLResponse) @app.get("/weather/{location}/days/mock", response_class=HTMLResponse)
async def get_weather_day(request: Request, location: str, day: datehelp.Day): async def get_weather_days_mock(request: Request):
date = datehelp.parse(day) response = MOCK_DATA.get_response("days")
response = await request.app.state.weather_api.get_day(location, date)
return build_weather_response(request, response) return build_weather_response(request, response)
@app.get("/weather/{location}/{date}", response_class=HTMLResponse) @app.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
async def get_weather(request: Request, location: str, date: datetime.date): async def get_weather_day(request: Request, location: str, date: datetime.date):
response = await request.app.state.weather_api.get_day(location, date) weather_api: WeatherApi = request.app.state.weather_api
response = await weather_api.get_day(location, date)
return build_weather_response(request, response)
@app.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
async def get_weather_days(request: Request, location: str, days: int):
weather_api: WeatherApi = request.app.state.weather_api
response = await weather_api.get_days(location, days)
return build_weather_response(request, response) return build_weather_response(request, response)

View File

@@ -50,6 +50,11 @@ td {
background: rgba(0, 128, 255, 0.2); background: rgba(0, 128, 255, 0.2);
} }
.date .value a {
all: unset;
cursor: pointer;
}
.cloudness { .cloudness {
vertical-align: top; vertical-align: top;
} }
@@ -62,11 +67,19 @@ td {
font-size: 2rem; font-size: 2rem;
} }
.temperature.positive .value { .temperature {
padding: 0;
}
.temperature .value {
padding: 0.1rem 0.4rem;
}
.temperature .value.positive {
color: orangered; color: orangered;
} }
.temperature.negative .value { .temperature .value.negative {
color: blue; color: blue;
} }
@@ -82,7 +95,12 @@ td {
color: blue; color: blue;
} }
.pressure {
padding: 0;
}
.pressure .value { .pressure .value {
padding: 0.1rem 0.4rem;
color: blueviolet; color: blueviolet;
} }

View File

@@ -17,21 +17,36 @@
<body> <body>
<h3> <h3>
{% if response.period == 'day' %}
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
href="{{response.date - datetime.timedelta(days=1)}}">⬅️</a> href="{{response.date - datetime.timedelta(days=1)}}">⬅️</a>
<a class="button"
href="../days/10">⬆️</a>
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span> <span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<a class="button" <a class="button"
href="{{response.date + datetime.timedelta(days=1)}}">➡️</a> href="{{response.date + datetime.timedelta(days=1)}}">➡️</a>
{% endif %}
{% if response.period == 'days' %}
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
{% endif %}
</h3> </h3>
<table> <table>
<tbody> <tbody>
<!-- date --> <!-- date -->
<tr> <tr>
{% for value in response.values %} {% for value in response.values %}
{% if response.period == 'day' %}
<td <td
class="date {{'now' if value.date < datetime.datetime.now() and value.date + datetime.timedelta(hours=3) > datetime.datetime.now() else ''}}"> class="date {{'now' if value.date < datetime.datetime.now() and value.date + datetime.timedelta(hours=3) > datetime.datetime.now() else ''}}">
<span class="value">{{value.date.strftime('%H:%M')}}</span> <span class="value">{{value.date.strftime('%H:%M')}}</span>
</td> </td>
{% endif %}
{% if response.period == 'days' %}
<td
class="date {{'now' if value.date.date() == datetime.date.today() else ''}}">
<span class="value"><a href="../day/{{value.date.date()}}">{{value.date.strftime('%a %d')}}</a></span>
</td>
{% endif %}
{% endfor %} {% endfor %}
</tr> </tr>
<!-- cloudness --> <!-- cloudness -->
@@ -59,9 +74,13 @@
</tr> </tr>
<tr> <tr>
{% for value in response.values %} {% for value in response.values %}
<td class="temperature {{'positive' if value.temperature > 0 else 'negative'}}" <td class="temperature">
style="background-color: rgba(255, 128, 128, {{(value.temperature - 10) * 0.015}});"> {% for temperature in value.temperature %}
<span class="value">{{value.temperature}}</span> <div class="value {{'positive' if temperature > 0 else 'negative'}}"
style="background-color: rgba(255, 128, 128, {{(temperature - 10) * 0.015}});">
{{temperature}}
</div>
{% endfor %}
</td> </td>
{% endfor %} {% endfor %}
</tr> </tr>
@@ -123,9 +142,12 @@
</tr> </tr>
<tr> <tr>
{% for value in response.values %} {% for value in response.values %}
<td class="pressure" <td class="pressure">
style="background-color: rgba(128, 0, 255, {{(value.pressure - 720) * 0.008}});"> {% for pressure in value.pressure %}
<span class="value">{{value.pressure}}</span> <div class="value"
style="background-color: rgba(128, 0, 255, {{(pressure - 720) * 0.008}});">
{{pressure}}</div>
{% endfor %}
</td> </td>
{% endfor %} {% endfor %}
</tr> </tr>

View File

@@ -45,12 +45,12 @@ class WindDirection(str, Enum):
class WeatherValue(Model): class WeatherValue(Model):
date: datetime.datetime date: datetime.datetime
sky: Sky sky: Sky
temperature: int temperature: list[int]
wind_speed: int wind_speed: int
wind_gust: int wind_gust: int
wind_direction: WindDirection wind_direction: WindDirection
precipitation: float precipitation: float
pressure: int pressure: list[int]
humidity: int humidity: int