From 7f0e19fb5ae17b6eb2453601b9665f1f264e4af4 Mon Sep 17 00:00:00 2001 From: shmyga Date: Mon, 29 Jul 2024 21:42:44 +0300 Subject: [PATCH] feat(api): add multiple days api --- gismeteo/api.py | 37 +- gismeteo/mock/__init__.py | 10 +- gismeteo/mock/data/10-days.html | 5246 +++++++++++++++++ gismeteo/mock/data/day.json | 1 + gismeteo/mock/data/days.json | 1 + .../mock/data/{weather.html => today.html} | 0 gismeteo/mock/data/weather.json | 1 - gismeteo/parser.py | 58 +- tests/test_gismeteo_api.py | 13 +- weather/api.py | 3 + weather/app/__init__.py | 1 - weather/app/route/api.py | 15 +- weather/app/route/view/__init__.py | 29 +- weather/app/route/view/static/style.css | 22 +- weather/app/route/view/templates/weather.html | 34 +- weather/model.py | 4 +- 16 files changed, 5411 insertions(+), 64 deletions(-) create mode 100644 gismeteo/mock/data/10-days.html create mode 100644 gismeteo/mock/data/day.json create mode 100644 gismeteo/mock/data/days.json rename gismeteo/mock/data/{weather.html => today.html} (100%) delete mode 100644 gismeteo/mock/data/weather.json diff --git a/gismeteo/api.py b/gismeteo/api.py index 28c13ee..f81ad79 100644 --- a/gismeteo/api.py +++ b/gismeteo/api.py @@ -9,15 +9,24 @@ from weather.model import WeatherResponse, WeatherValue from . import datehelp 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): 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: 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: return await response.text() @@ -39,7 +48,31 @@ class GismeteoApi(WeatherApi): 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: location = LOCATION_BUNDLE.parse(location_id) data = await self._request(f"weather-{location}/{datehelp.dump(date)}") 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) diff --git a/gismeteo/mock/__init__.py b/gismeteo/mock/__init__.py index cf53c87..65fdb84 100644 --- a/gismeteo/mock/__init__.py +++ b/gismeteo/mock/__init__.py @@ -6,13 +6,11 @@ from gismeteo.api import WeatherResponse class MockData: - @property - def html(self) -> str: - return (Path(__file__).parent / "data/weather.html").read_text() + def get_html(self, key: str) -> str: + return (Path(__file__).parent / f"data/{key}.html").read_text() - @property - def response(self) -> WeatherResponse: - data = json.loads((Path(__file__).parent / "data/weather.json").read_text()) + def get_response(self, key: str) -> WeatherResponse: + data = json.loads((Path(__file__).parent / f"data/{key}.json").read_text()) return WeatherResponse(**data) diff --git a/gismeteo/mock/data/10-days.html b/gismeteo/mock/data/10-days.html new file mode 100644 index 0000000..301f364 --- /dev/null +++ b/gismeteo/mock/data/10-days.html @@ -0,0 +1,5246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GISMETEO: Погода в Змиевке на 10 дней, прогноз погоды Змиевка на десять дней, Свердловский район, Орловская область, Россия + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
Перейти на мобильную версию
+
+ + + +
+
+
+ + + + + + + + +
+ +
+ +
+ + + + +
+ +
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + Погода + + + + + Новости + + + + Карты + + + + Приложения + + +
+
+ +
+
+ +
+
+ + + + + + + + +
+
+ +
+ + + + + + +
+ + +
+ + + + + + + + +
+ + + + + + + +
+
+ + + + + +
+ +

Погода в Змиевке на 10 дней

+ +
+
+ + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + +
+ +
+ +
+ + +
+ + + + + + +
+ +
+ +
+ + +
+ + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + + +
+ +
+ +
+ + + + + + +
+ +

+ + Температура воздуха, +

+ + +
+
+ + + + + + + +
+ +

+ + Температура по ощущению, +

+ + +
+
+ + + + + + + +
+ +

+ + Среднесуточная температура, +

+ + +
+
+ + + + + + + +
+ +

+ + Средняя скорость ветра, +

+ + + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + + + + + + +
+ +

+ + Порывы ветра, +

+ + + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + + + + + + +
+ +

+ + Направление ветра +

+ + + +
+ + +
+ + + +
+
З
+ +
+ +
+ + +
+ + + +
+
С
+ +
+ +
+ + +
+ + + +
+
СЗ
+ +
+ +
+ + +
+ + + +
+
З
+ +
+ +
+ + +
+ + + +
+
З
+ +
+ +
+ + +
+ + + +
+
СЗ
+ +
+ +
+ + +
+ + + +
+
С
+ +
+ +
+ + +
+ + + +
+
СЗ
+ +
+ +
+ + +
+ + + +
+
СЗ
+ +
+ +
+ + +
+ + + +
+
СЗ
+ +
+ +
+ + + + + + +
+ + + + + + +
+
+ 0 +
+ + + +
+ + + +
+
+ 0 +
+ + + +
+ + + +
+
+ 0 +
+ + + +
+ + + +
+
+ 0 +
+ + + +
+ + + +
+
+ 0 +
+ + + +
+ + + +
+
+ 0 +
+ + + +
+ + + +
+
+ 0 +
+ + + +
+ + + +
+
+ 0 +
+ + + +
+ + + +
+
+
+ + + +
+
+
+ + +
+ + + + + + + +
+ + + + + + +
+
+ 1 +
+ + + +
+ + + +
+
+ 1 +
+ + + +
+ + + +
+
+ 1 +
+ + + +
+ + + +
+
+ 1 +
+ + + +
+ + + +
+
+ 1 +
+ + + +
+ + + +
+
+ 1 +
+ + + +
+ + + +
+
+ 1 +
+ + + +
+ + + +
+
+ 1 +
+ + + +
+ + + +
+
+
+ + + +
+
+
+ + +
+ + + + + + + +
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + +
+ + + + + + + +
+ + + + + + + + + + + +
+ + + + + + +
5,4
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + +
11,6
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + +
1,1
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + +
0,1
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
0
+ +
+ +
+ +
0
+ +
+ +
+ +
0
+ +
+ +
+ + + + + + +
1,9
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + +
0,4
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + +
0,1
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + +
+ + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + + + + +
+ + + + +
0
0
0
0
0
0
0
0
0
0
+
+ + + + + + + + + +
+ +

+ + Давление, +

+ + +
+
+ + + + + + + +
+ +

+ + Относительная влажность, % +

+ + + +
+ 93 +
+ +
+ 100 +
+ +
+ 97 +
+ +
+ 96 +
+ +
+ 75 +
+ +
+ 92 +
+ +
+ 86 +
+ +
+ 95 +
+ +
+ 95 +
+ +
+ 92 +
+ +
+ + + + + + +
+ + + + + + + +
+ 8 +
+ + + + + +
+ 2 +
+ + + + + +
+ 4 +
+ + + + + +
+ 7 +
+ + + + + +
+ 6 +
+ + + + + +
+ 4 +
+ + + + + +
+ 6 +
+ + + + + +
+ 5 +
+ + + + + +
+ 5 +
+ + + + + +
+ 6 +
+ + + +
+ + + + + + +
+ + + + + +
+ + + +
+ 5 +
+
+ +
+ + + +
+ 7 +
+
+ +
+ + + +
+ 6 +
+
+ +
+ + + +
+ 3 +
+
+ +
+ + + +
+ 3 +
+
+ +
+ + + +
+ 2 +
+
+ +
+ + + +
+ 2 +
+
+ +
+ + + +
+ 2 +
+
+ +
+ + + +
+ 2 +
+
+ +
+ + + +
+ 2 +
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + +
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + +
+ +
+ + +
+ + + + + + + + + + +
+
+ +
+ + +
+
+ + осадки + +
+ +
+ + + + + + + +
+
+ + ветер + +
+ +
+ + + + + + + + +
+
+
+ + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + +
+ + + + + + +
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + +
+ +
+ + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + +
+ + +
+ События в мире +
+ + +
+ + + + + + + + + + +
+
+
+
Китай исключил Россию из проекта дальнемагистрального самолета
+ + + + + +
+ + + + + + + + + +
+
+
+
Опубликована последняя радиограмма бойцов ЧВК «Вагнер» в Мали
+ + + + + +
+ + + + + + + + + +
+
+
+
Украинская фехтовальщица на Олимпиаде обняла россиянку
+ + + + + +
+ + + + + + + + + +
+
+
+
Зеленский анонсировал «обсуждение территориальной целостности»
+ + + + + +
+ + + + + + + + + +
+
+
+
Военкор Рожин: Преемник Пригожина Лотос попал в плен в Мали
+ + + + + +
+ + + + + + + + + +
+
+
+
Человеку впервые пересадили полностью искусственное сердце
+ + + + + +
+ + + + + + + + + +
+
+
+
Эрдоган заявил, что Турция может ввести войска в Израиль
+ + + + + +
+ + + + + + + + + +
+
+
+
Французы извинились за пародию на «Тайную вечерю» на открытии Игр
+ + + + + +
+ + + + + + + + + +
+
+
+
Российским компаниям стали возвращать до 80% платежей в юанях
+ + + + + +
+ + + + + + + + + +
+
+
+
Путин предупредил США об ответе на размещение ракет в Германии
+ + + + + +
+ + + + + + + + + +
+
+
+
Больше половины россиян заявили о вынужденных переработках
+ + + + + +
+ + + + + + + + + +
+
+
+
В Карелии из-за размытия перемычки ББК введен режим ЧС
+ + + + + +
+ + + + + + + + + +
+
+
+
Оппозиция Венесуэлы не признала победу Мадуро на выборах
+ + + + + +
+ + + + + + + + + +
+
+
+
Япония призвала граждан не ездить в «опасную Россию»
+ + + + + +
+ + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + +
+ + + + + + + + + + + + + + +
+ + +
+ Лента новостей +
+ + +
+ + + + + + + + + + + +
+
+
+
Еще 4 области должны войти в состав России — Сальдо
+ + + + + + +
+ + + + + + + + + +
ВСУ лишились самой опасной ракетной установки М270 MLRS
+ + + + + + +
+ + + + + + + + + +
Россиянам рассказали о новом порядке экзаменов на права
+ + + + + + +
+ + + + + + + + + + +
+
+
+
США вывели бомбардировщики B-52H с авиабазы вблизи Украины
+ + + + + + +
+ + + + + + + + + +
Новый маневр позволил ВС РФ продвинуться по всему фронту
+ + + + + + +
+ + + + + + + + + +
F-16 столкнутся с проблемами из-за России и нехватки экипажей
+ + + + + + +
+ + + + + + + + + + +
+
+
+
ВСУ потеряли линию укреплений у Славянска — Краматорска
+ + + + + + +
+ + + + + + + + + +
Найдены доказательства работы украинских наемников в Африке
+ + + + + + +
+ + + + + + + + + +
Российские истребители оснастят беспилотниками
+ + + + + + +
+ + + + + + + + + + +
+
+
+
Украину предупредили о графиках включений электроэнергии
+ + + + + + +
+ + + + + + + + + +
Функционал истребителя Су-57 расширят под задачи СВО
+ + + + + + +
+ + + + + + + + + +
МО: Российские военные освободили Прогресс и Евгеновку в ДНР
+ + + + + + +
+ + + + + + + + + + +
+
+
+
Япония поставит США ракеты для комплексов Patriot PAC-3
+ + + + + + +
+ + + + + + + + + +
ВСУ признали Покровское направление самым напряженным
+ + + + + + +
+ + + + + + + + + +
Путин готовится ко встрече с новым президентом Ирана осенью
+ + + + + + +
+ + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + +
+ + +
+ Объясняем.рф +
+ + +
+ + + + + + + + + + + +
+
+
+
Как восстанавливают железнодорожное сообщение после аварии под Волгоградом?
+ + + + + + +
+ + + + + + + + + +
Какие самые популярные специальности в вузах в этом году?
+ + + + + + +
+ + + + + + + + + +
Какое российское лекарство способно остановить развитие болезни Бехтерева?
+ + + + + + +
+ + + + + + + + + + +
+
+
+
Будут ли туроператоры штрафовать туристов за отмену броней из-за аварии под Волгоградом?
+ + + + + + +
+ + + + + + + + + +
Как будут финансировать первую в России высокоскоростную железнодорожную магистраль?
+ + + + + + +
+ + + + + + + + + +
Как российские школьники справились на Международной олимпиаде по химии?
+ + + + + + +
+ + + + + + + + + + +
+
+
+
Как будет развиваться ядерная медицина?
+ + + + + + +
+ + + + + + + + + +
Какое приложение поможет удобно спланировать путешествие?
+ + + + + + +
+ + + + + + + + + +
Чем будет заниматься Морская коллегия?
+ + + + + + +
+ + + + + + + + + + +
+
+
+
Как будет развиваться минерально-сырьевая база РФ?
+ + + + + + +
+ + + + +
Социальная реклама
+ + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + diff --git a/gismeteo/mock/data/day.json b/gismeteo/mock/data/day.json new file mode 100644 index 0000000..7588c49 --- /dev/null +++ b/gismeteo/mock/data/day.json @@ -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}]} \ No newline at end of file diff --git a/gismeteo/mock/data/days.json b/gismeteo/mock/data/days.json new file mode 100644 index 0000000..655b794 --- /dev/null +++ b/gismeteo/mock/data/days.json @@ -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}]} \ No newline at end of file diff --git a/gismeteo/mock/data/weather.html b/gismeteo/mock/data/today.html similarity index 100% rename from gismeteo/mock/data/weather.html rename to gismeteo/mock/data/today.html diff --git a/gismeteo/mock/data/weather.json b/gismeteo/mock/data/weather.json deleted file mode 100644 index f089bd8..0000000 --- a/gismeteo/mock/data/weather.json +++ /dev/null @@ -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}]} \ No newline at end of file diff --git a/gismeteo/parser.py b/gismeteo/parser.py index 9e44133..33e5a93 100644 --- a/gismeteo/parser.py +++ b/gismeteo/parser.py @@ -1,6 +1,6 @@ import datetime import re -from typing import Dict, Iterable, List, Optional +from typing import Iterable import dateparser from bs4 import Tag @@ -10,12 +10,13 @@ from weather.model import Cloudness, Precipitation, Sky, WindDirection from .core import BaseWidgetParser, RowParser ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items") +DAYS_PARSER = BaseWidgetParser(".widget.widget-days .widget-items") class LocationParser: 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) if match: return match.group(1) @@ -29,30 +30,35 @@ class DateParser(RowParser[datetime.datetime]): KEY = "date" def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]: - date_str = ( - tag.select_one(".widget-row.widget-row-datetime-date > .row-item") - .find(text=True, recursive=False) - .text + datetime_date_tag = tag.select_one( + ".widget-row.widget-row-datetime-date > .row-item" ) - date = dateparser.parse(date_str, languages=["ru"]) - for item in tag.select(".widget-row.widget-row-datetime-time > .row-item"): - time_str = item.text - time = dateparser.parse(time_str, languages=["ru"]) - time = time.replace(year=date.year, month=date.month, day=date.day) - yield time + if datetime_date_tag: + date_str = datetime_date_tag.find(text=True, recursive=False).text + date = dateparser.parse(date_str, languages=["ru"]) + for item in tag.select(".widget-row.widget-row-datetime-time > .row-item"): + time_str = item.text + 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]): KEY = "sky" - CLOUDNESS_MAP: Dict[str, Cloudness] = { + CLOUDNESS_MAP: dict[str, Cloudness] = { "ясно": Cloudness.CLEAR, "малооблачно": Cloudness.PARTLY_CLOUDY, "облачно": Cloudness.CLOUDY, "пасмурно": Cloudness.MAINLY_CLOUDY, } - PRECIPITATION_MAP: Dict[str, Precipitation] = { + PRECIPITATION_MAP: dict[str, Precipitation] = { "без осадков": Precipitation.NO, "небольшой дождь": Precipitation.SMALL_RAIN, "дождь": Precipitation.RAIN, @@ -83,14 +89,16 @@ class SkyParser(RowParser[Sky]): ) -class TemperatureParser(RowParser[int]): +class TemperatureParser(RowParser[list[int]]): KEY = "temperature" - def parse_row(self, tag: Tag) -> Iterable[int]: + def parse_row(self, tag: Tag) -> Iterable[list[int]]: 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]): @@ -115,7 +123,7 @@ class WindGustParser(RowParser[int]): class WindDirectionParser(RowParser[WindDirection]): KEY = "wind_direction" - WIND_DIRECTION_MAP: Dict[str, WindDirection] = { + WIND_DIRECTION_MAP: dict[str, WindDirection] = { "штиль": WindDirection.CALM, "с": WindDirection.N, "св": WindDirection.NO, @@ -145,14 +153,14 @@ class WindPrecipitationParser(RowParser[float]): yield float(item.text.replace(",", ".")) -class PressureParser(RowParser[int]): +class PressureParser(RowParser[list[int]]): KEY = "pressure" - def parse_row(self, tag: Tag) -> Iterable[int]: + def parse_row(self, tag: Tag) -> Iterable[list[int]]: 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]): @@ -163,7 +171,7 @@ class HumidityParser(RowParser[int]): yield int(item.text) -ROW_PARSERS: List[RowParser] = [ +ROW_PARSERS: list[RowParser] = [ DateParser(), SkyParser(), TemperatureParser(), @@ -175,4 +183,4 @@ ROW_PARSERS: List[RowParser] = [ 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} diff --git a/tests/test_gismeteo_api.py b/tests/test_gismeteo_api.py index 266dbe8..0e4740a 100644 --- a/tests/test_gismeteo_api.py +++ b/tests/test_gismeteo_api.py @@ -11,14 +11,17 @@ def gismeteo_api_fixture() -> GismeteoApi: api = GismeteoApi() async def _request(endpoint: str) -> str: - return MOCK_DATA.html + return MOCK_DATA.get_html(endpoint.split("/")[-1]) api._request = _request return api -async def test_api(gismeteo_api: GismeteoApi): - result = await gismeteo_api.get_day( - "zmiyevka", datetime.date.today() + datetime.timedelta(days=1) - ) +async def test_day(gismeteo_api: GismeteoApi): + result = await gismeteo_api.get_day("zmiyevka", datetime.date.today()) 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 diff --git a/weather/api.py b/weather/api.py index 3101caf..cf9f808 100644 --- a/weather/api.py +++ b/weather/api.py @@ -6,3 +6,6 @@ from .model import WeatherResponse class WeatherApi: async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse: raise NotImplementedError + + async def get_days(self, location_id: str, days: int) -> WeatherResponse: + raise NotImplementedError diff --git a/weather/app/__init__.py b/weather/app/__init__.py index 0f9b1c8..debba70 100644 --- a/weather/app/__init__.py +++ b/weather/app/__init__.py @@ -9,7 +9,6 @@ from .route import api, doc, view def build_app(weather_api: WeatherApi) -> FastAPI: locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") - app = FastAPI( title="Weather", docs_url=None, diff --git a/weather/app/route/api.py b/weather/app/route/api.py index e4b016b..969b65f 100644 --- a/weather/app/route/api.py +++ b/weather/app/route/api.py @@ -2,12 +2,21 @@ import datetime from fastapi import FastAPI, Request +from weather.api import WeatherApi from weather.model import WeatherResponse def mount(app: FastAPI): - @app.get("/api/weather/{location}/{date}") - async def get_weather_api( + @app.get("/api/weather/{location}/day/{date}") + async def get_api_weather_day( request: Request, location: str, date: datetime.date ) -> 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) diff --git a/weather/app/route/view/__init__.py b/weather/app/route/view/__init__.py index e28bc14..bff7afd 100644 --- a/weather/app/route/view/__init__.py +++ b/weather/app/route/view/__init__.py @@ -9,6 +9,7 @@ from fastapi.templating import Jinja2Templates from gismeteo import datehelp from gismeteo.location import LOCATION_BUNDLE from gismeteo.mock import MOCK_DATA +from weather.api import WeatherApi from weather.model import WeatherResponse from .filters import cloudness_icon, wind_direction_icon @@ -47,20 +48,26 @@ def mount(app: FastAPI): @app.get("/weather/{location}", response_class=RedirectResponse) 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) - async def get_weather_mock(request: Request): - response = MOCK_DATA.response + @app.get("/weather/{location}/day/mock", response_class=HTMLResponse) + async def get_weather_day_mock(request: Request): + response = MOCK_DATA.get_response("day") return build_weather_response(request, response) - # @app.get("/weather/{location}/{day}", response_class=HTMLResponse) - async def get_weather_day(request: Request, location: str, day: datehelp.Day): - date = datehelp.parse(day) - response = await request.app.state.weather_api.get_day(location, date) + @app.get("/weather/{location}/days/mock", response_class=HTMLResponse) + async def get_weather_days_mock(request: Request): + response = MOCK_DATA.get_response("days") return build_weather_response(request, response) - @app.get("/weather/{location}/{date}", response_class=HTMLResponse) - async def get_weather(request: Request, location: str, date: datetime.date): - response = await request.app.state.weather_api.get_day(location, date) + @app.get("/weather/{location}/day/{date}", response_class=HTMLResponse) + async def get_weather_day(request: Request, location: str, date: datetime.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) diff --git a/weather/app/route/view/static/style.css b/weather/app/route/view/static/style.css index df88645..a3c8e99 100644 --- a/weather/app/route/view/static/style.css +++ b/weather/app/route/view/static/style.css @@ -50,6 +50,11 @@ td { background: rgba(0, 128, 255, 0.2); } +.date .value a { + all: unset; + cursor: pointer; +} + .cloudness { vertical-align: top; } @@ -62,11 +67,19 @@ td { font-size: 2rem; } -.temperature.positive .value { +.temperature { + padding: 0; +} + +.temperature .value { + padding: 0.1rem 0.4rem; +} + +.temperature .value.positive { color: orangered; } -.temperature.negative .value { +.temperature .value.negative { color: blue; } @@ -82,7 +95,12 @@ td { color: blue; } +.pressure { + padding: 0; +} + .pressure .value { + padding: 0.1rem 0.4rem; color: blueviolet; } diff --git a/weather/app/route/view/templates/weather.html b/weather/app/route/view/templates/weather.html index d524bbf..34422fd 100644 --- a/weather/app/route/view/templates/weather.html +++ b/weather/app/route/view/templates/weather.html @@ -17,21 +17,36 @@

+ {% if response.period == 'day' %} ⬅️ + ⬆️ {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}} ➡️ + {% endif %} + {% if response.period == 'days' %} + {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}} + {% endif %}

{% for value in response.values %} + {% if response.period == 'day' %} + {% endif %} + {% if response.period == 'days' %} + + {% endif %} {% endfor %} @@ -59,9 +74,13 @@ {% for value in response.values %} - {% endfor %} @@ -123,9 +142,12 @@ {% for value in response.values %} - {% endfor %} diff --git a/weather/model.py b/weather/model.py index ababc40..d8532ff 100644 --- a/weather/model.py +++ b/weather/model.py @@ -45,12 +45,12 @@ class WindDirection(str, Enum): class WeatherValue(Model): date: datetime.datetime sky: Sky - temperature: int + temperature: list[int] wind_speed: int wind_gust: int wind_direction: WindDirection precipitation: float - pressure: int + pressure: list[int] humidity: int
{{value.date.strftime('%H:%M')}} + {{value.date.strftime('%a %d')}} +
- {{value.temperature}} + + {% for temperature in value.temperature %} +
+ {{temperature}} +
+ {% endfor %}
- {{value.pressure}} + + {% for pressure in value.pressure %} +
+ {{pressure}}
+ {% endfor %}