feat: split to weather and gismeteo modules

This commit is contained in:
2024-07-26 11:02:01 +03:00
parent c9e52c43a9
commit 848b6bd9ba
28 changed files with 157 additions and 108 deletions

View File

@@ -1,27 +1,18 @@
import datetime
from typing import Any, Dict, List, NamedTuple
from typing import Any, Dict, List
import aiohttp
from bs4 import BeautifulSoup
from . import dateutil
from weather.api import WeatherApi
from weather.model import WeatherResponse, WeatherValue
from . import datehelp
from .location import LOCATION_BUNDLE
from .parser import ROW_PARSERS, ONE_DAY_PARSER
from .parser import LOCATION_PARSER, ONE_DAY_PARSER, ROW_PARSERS
class WeatherValue(NamedTuple):
date: datetime.datetime
cloudness: str
temperature: int
wind_speed: int
wind_gust: int
wind_direction: str
precipitation: float
pressure: int
humidity: int
class GismeteoApi:
class GismeteoApi(WeatherApi):
BASE_URL = "https://www.gismeteo.ru"
async def _request(self, endpoint: str) -> str:
@@ -30,18 +21,25 @@ class GismeteoApi:
async with session.request("GET", url) as response:
return await response.text()
def _parse_oneday(self, data: str) -> List[WeatherValue]:
def _parse_oneday(self, date: datetime.date, data: str) -> WeatherResponse:
result: List[Dict[str, Any]] = []
soup = BeautifulSoup(data, features="html.parser")
location = LOCATION_PARSER.parse_location(data)
widget = ONE_DAY_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
return [WeatherValue(**item) for item in result]
values = [WeatherValue(**item) for item in result]
return WeatherResponse(
location=location or "n/a",
date=date,
period="day",
values=values,
)
async def get_day(self, location_id: str, date: datetime.date) -> List[WeatherValue]:
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}/{dateutil.dump(date)}")
return self._parse_oneday(data)
data = await self._request(f"weather-{location}/{datehelp.dump(date)}")
return self._parse_oneday(date, data)

View File

@@ -1,20 +0,0 @@
import locale
from os import environ
import uvicorn
from fastapi import FastAPI
from gismeteo.route import api, doc, view
# locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
app = FastAPI(docs_url=None, redoc_url=None)
doc.mount(app)
api.mount(app)
view.mount(app)
def run():
uvicorn.run(
"gismeteo.app:app", host="0.0.0.0", port=8000, reload="DEBUG" in environ
)

View File

@@ -4,7 +4,6 @@ from bs4 import Tag
T = TypeVar("T")
class WidgetParser:
def parse_widget(self, tag: Tag) -> Tag:
raise NotImplementedError

View File

@@ -1,10 +1,7 @@
import json
from pathlib import Path
from typing import List
import dateparser
from gismeteo.api import WeatherValue
from gismeteo.api import WeatherResponse
class MockData:
@@ -14,12 +11,9 @@ class MockData:
return (Path(__file__).parent / "data/weather.html").read_text()
@property
def values(self) -> List[WeatherValue]:
def response(self) -> WeatherResponse:
data = json.loads((Path(__file__).parent / "data/weather.json").read_text())
return [
WeatherValue(**{**item, "date": dateparser.parse(item["date"])})
for item in data
]
return WeatherResponse(**data)
MOCK_DATA = MockData()

View File

@@ -1 +1 @@
[{"date":"2024-07-25T00:00:00","cloudness":"Ясно","temperature":14,"wind_speed":1,"wind_gust":1,"wind_direction":"С","precipitation":0.0,"pressure":739,"humidity":85},{"date":"2024-07-25T03:00:00","cloudness":"Ясно","temperature":13,"wind_speed":1,"wind_gust":2,"wind_direction":"СЗ","precipitation":0.0,"pressure":739,"humidity":92},{"date":"2024-07-25T06:00:00","cloudness":"Малооблачно, без осадков","temperature":14,"wind_speed":1,"wind_gust":2,"wind_direction":"СЗ","precipitation":0.0,"pressure":738,"humidity":89},{"date":"2024-07-25T09:00:00","cloudness":"Малооблачно, без осадков","temperature":23,"wind_speed":3,"wind_gust":5,"wind_direction":"С","precipitation":0.0,"pressure":738,"humidity":58},{"date":"2024-07-25T12:00:00","cloudness":"Малооблачно, без осадков","temperature":26,"wind_speed":3,"wind_gust":6,"wind_direction":"СЗ","precipitation":0.0,"pressure":738,"humidity":47},{"date":"2024-07-25T15:00:00","cloudness":"Облачно, без осадков","temperature":26,"wind_speed":2,"wind_gust":6,"wind_direction":"С","precipitation":0.0,"pressure":737,"humidity":46},{"date":"2024-07-25T18:00:00","cloudness":"Малооблачно, небольшой дождь","temperature":24,"wind_speed":3,"wind_gust":7,"wind_direction":"СВ","precipitation":0.3,"pressure":737,"humidity":54},{"date":"2024-07-25T21:00:00","cloudness":"Ясно","temperature":19,"wind_speed":1,"wind_gust":5,"wind_direction":"С","precipitation":0.0,"pressure":738,"humidity":80}]
{"location":"Змиевка","date":"2024-07-26","period":"day","values":[{"date":"2024-07-26T00:00:00","cloudness":"Облачно, без осадков","temperature":15,"wind_speed":1,"wind_gust":1,"wind_direction":"СВ","precipitation":0.0,"pressure":738,"humidity":96},{"date":"2024-07-26T03:00:00","cloudness":"Ясно","temperature":14,"wind_speed":0,"wind_gust":1,"wind_direction":"штиль","precipitation":0.0,"pressure":738,"humidity":98},{"date":"2024-07-26T06:00:00","cloudness":"Ясно","temperature":15,"wind_speed":1,"wind_gust":1,"wind_direction":"С","precipitation":0.0,"pressure":738,"humidity":97},{"date":"2024-07-26T09:00:00","cloudness":"Ясно","temperature":22,"wind_speed":1,"wind_gust":3,"wind_direction":"СВ","precipitation":0.0,"pressure":739,"humidity":66},{"date":"2024-07-26T12:00:00","cloudness":"Малооблачно, без осадков","temperature":24,"wind_speed":2,"wind_gust":5,"wind_direction":"СВ","precipitation":0.0,"pressure":739,"humidity":47},{"date":"2024-07-26T15:00:00","cloudness":"Облачно, без осадков","temperature":25,"wind_speed":2,"wind_gust":5,"wind_direction":"СВ","precipitation":0.0,"pressure":739,"humidity":40},{"date":"2024-07-26T18:00:00","cloudness":"Облачно, без осадков","temperature":25,"wind_speed":2,"wind_gust":5,"wind_direction":"СВ","precipitation":0.0,"pressure":740,"humidity":39},{"date":"2024-07-26T21:00:00","cloudness":"Ясно","temperature":18,"wind_speed":1,"wind_gust":5,"wind_direction":"СВ","precipitation":0.0,"pressure":741,"humidity":62}]}

View File

@@ -1,5 +1,6 @@
import datetime
from typing import Dict, Iterable, List
import re
from typing import Dict, Iterable, List, Optional
import dateparser
from bs4 import Tag
@@ -9,6 +10,19 @@ from .core import BaseWidgetParser, RowParser
ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items")
class LocationParser:
PATTERN = re.compile('{"ru":{"city":{"name":"(.*?)"')
def parse_location(self, data: str) -> Optional[str]:
match = self.PATTERN.search(data)
if match:
return match.group(1)
return None
LOCATION_PARSER = LocationParser()
class DateParser(RowParser[datetime.datetime]):
KEY = "date"

View File

@@ -1,12 +0,0 @@
from fastapi import FastAPI
from gismeteo import dateutil
from gismeteo.api import GismeteoApi
def mount(app: FastAPI):
@app.get("/api/weather/{location}/{date}")
async def get_weather(location: str, date: str):
api = GismeteoApi()
result = await api.get_day(location, dateutil.parse(date))
return [item._asdict() for item in result]

View File

@@ -1,37 +0,0 @@
from anyio import Path
from fastapi import FastAPI
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from fastapi.staticfiles import StaticFiles
def mount(app: FastAPI):
app.mount(
"/docs/static",
StaticFiles(directory=Path(__file__).parent / "static"),
)
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="docs/static/swagger-ui-bundle.js",
swagger_css_url="docs/static/swagger-ui.css",
)
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
return get_redoc_html(
openapi_url=app.openapi_url,
title=app.title + " - ReDoc",
redoc_js_url="docs/static/redoc.standalone.js",
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,43 +0,0 @@
import datetime
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from gismeteo import dateutil
from gismeteo.api import GismeteoApi
from gismeteo.mock import MOCK_DATA
from .filters import cloudness_icon, wind_direction_icon
def mount(app: FastAPI):
base_dir = Path(__file__).parent
app.mount("/static", StaticFiles(directory=base_dir / "static"), name="static")
templates = Jinja2Templates(directory=base_dir / "templates")
templates.env.filters["wind_direction_icon"] = wind_direction_icon
templates.env.filters["cloudness_icon"] = cloudness_icon
@app.get("/weather/{location}")
async def get_weather_base(location: str):
return RedirectResponse(f"{location}/today")
@app.get("/weather/{location}/{date}", response_class=HTMLResponse)
async def get_weather(request: Request, location: str, date: str):
if date == "mock":
values = MOCK_DATA.values
else:
api = GismeteoApi()
values = await api.get_day(location, dateutil.parse(date))
return templates.TemplateResponse(
request=request,
name="weather.html",
context={
"datetime": datetime,
"location": location,
"date": dateutil.parse(date),
"values": values,
},
)

View File

@@ -1,27 +0,0 @@
def wind_direction_icon(wind_direction: str) -> str:
return {
"С": "🡫",
"СВ": "🡯",
"В": "🡨",
"ЮВ": "🡬",
"Ю": "🡡",
"ЮЗ": "🡭",
"З": "🡪",
"СЗ": "🡦",
"штиль": "",
}.get(wind_direction, wind_direction)
def cloudness_icon(cloudness: str) -> str:
return {
"Ясно": "☀️",
"Малооблачно, без осадков": "🌤️",
"Облачно, без осадков": "",
"Малооблачно, небольшой дождь": "🌦️",
"Пасмурно, без осадков": "☁️",
"Облачно, небольшой дождь": "🌧️",
"Облачно, дождь": "🌧️",
"Облачно, небольшой дождь, гроза": "⛈️",
"Малооблачно, дождь": "🌦️",
"Пасмурно, небольшой дождь": "🌧️",
}.get(cloudness, cloudness)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,81 +0,0 @@
body {
font-size: 1.5rem;
}
h3 {
margin: 0.5rem 0;
}
a.button {
text-decoration: none;
color: inherit;
}
.button.disabled {
pointer-events: none;
cursor: default;
color: gray;
}
table {
/* width: 100%; */
table-layout: fixed;
border-collapse: collapse;
}
table,
th,
td {
/* border: 1px solid rgba(0, 0, 0, 0.2); */
text-align: center;
}
td {
padding: 0.1rem 0.4rem;
}
.header {
font-size: 1rem;
text-align: left;
}
.date {
font-size: 1.5rem;
background: rgba(0, 0, 0, 0.1);
}
.date.now {
background: rgba(0, 128, 255, 0.2);
}
.cloudness .icon {
font-size: 2rem;
}
.temperature.positive .value {
color: orangered;
}
.temperature.negative .value {
color: blue;
}
.wind .direction {
font-size: 1rem;
}
.wind .gust {
font-size: 1rem;
}
.precipitation .value {
color: blue;
}
.pressure .value {
color: blueviolet;
}
.humidity .value {
color: blue;
}

View File

@@ -1,151 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible"
content="ie=edge">
<title>Weather - {{date.strftime('%a, %d %B %Y')}}</title>
<link rel="stylesheet"
href="/static/style.css">
<link rel="icon"
href="/static/favicon.ico"
type="image/x-icon">
</head>
<body>
<h3>
<a class="button {{'disabled' if date == datetime.date.today() else ''}}"
href="{{date - datetime.timedelta(days=1)}}">🡨</a>
<span>{{date.strftime('%a, %d %B %Y')}}</span>
<a class="button"
href="{{date + datetime.timedelta(days=1)}}">🡪</a>
</h3>
<table>
<tbody>
<!-- date -->
<tr>
{% for value in values %}
<td
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>
</td>
{% endfor %}
</tr>
<!-- cloudness -->
<tr>
<td colspan="{{values | length}}"
class="header">
Облачность
</td>
</tr>
<tr>
{% for value in values %}
<td class="cloudness">
<span class="icon">{{value.cloudness | cloudness_icon}}</span>
</td>
{% endfor %}
</tr>
<!-- temperature -->
<tr>
<td colspan="{{values | length}}"
class="header">
Температура, °C
</td>
</tr>
<tr>
{% for value in values %}
<td class="temperature {{'positive' if value.temperature > 0 else 'negative'}}"
style="background-color: rgba(255, 128, 128, {{(value.temperature - 10) * 0.015}});">
<span class="value">{{value.temperature}}</span>
</td>
{% endfor %}
</tr>
<!-- wind_direction -->
<tr>
<td colspan="{{values | length}}"
class="header">
Направление ветра
</td>
</tr>
<tr>
{% for value in values %}
<td class="wind">
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
<span class="direction">{{value.wind_direction}}</span>
</td>
{% endfor %}
</tr>
<!-- wind_speed -->
<tr>
<td colspan="{{values | length}}"
class="header">
Скорость ветра, м/с
</td>
</tr>
<tr>
{% for value in values %}
<td class="wind"
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});">
<span class="speed">{{value.wind_speed}}</span>
{% if value.wind_gust != value.wind_speed %}
<span class="gust">
({{value.wind_gust}})
</span>
{% endif %}
</td>
{% endfor %}
</tr>
<!-- precipitation -->
<tr>
<td colspan="{{values | length}}"
class="header">
Осадки, мм
</td>
</tr>
<tr>
{% for value in values %}
<td class="precipitation"
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
<span class="value">{{value.precipitation or ''}}</span>
</td>
{% endfor %}
</tr>
<!-- pressure -->
<tr>
<td colspan="{{values | length}}"
class="header">
Давление, мм рт. ст.
</td>
</tr>
<tr>
{% for value in values %}
<td class="pressure"
style="background-color: rgba(128, 0, 255, {{(value.pressure - 720) * 0.008}});">
<span class="value">{{value.pressure}}</span>
</td>
{% endfor %}
</tr>
<!-- humidity -->
<tr>
<td colspan="{{values | length}}"
class="header">
Влажность, %
</td>
</tr>
<tr>
{% for value in values %}
<td class="humidity"
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
<span class="value">{{value.humidity}}</span>
</td>
{% endfor %}
</tr>
</tbody>
</table>
<script src="/static/index.js"></script>
</body>
</html>