feat(api): add multiple days api
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5246
gismeteo/mock/data/10-days.html
Normal file
5246
gismeteo/mock/data/10-days.html
Normal file
File diff suppressed because one or more lines are too long
1
gismeteo/mock/data/day.json
Normal file
1
gismeteo/mock/data/day.json
Normal 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}]}
|
||||||
1
gismeteo/mock/data/days.json
Normal file
1
gismeteo/mock/data/days.json
Normal 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}]}
|
||||||
@@ -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}]}
|
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user