feat: split to weather and gismeteo modules
This commit is contained in:
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"python.testing.pytestArgs": [
|
"python.testing.pytestArgs": ["tests", "-s"],
|
||||||
"tests", "-s"
|
"python.testing.unittestEnabled": false,
|
||||||
],
|
"python.testing.pytestEnabled": true,
|
||||||
"python.testing.unittestEnabled": false,
|
"files.exclude": {
|
||||||
"python.testing.pytestEnabled": true
|
"**/__pycache__": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,18 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Any, Dict, List, NamedTuple
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from bs4 import BeautifulSoup
|
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 .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):
|
class GismeteoApi(WeatherApi):
|
||||||
date: datetime.datetime
|
|
||||||
cloudness: str
|
|
||||||
temperature: int
|
|
||||||
wind_speed: int
|
|
||||||
wind_gust: int
|
|
||||||
wind_direction: str
|
|
||||||
precipitation: float
|
|
||||||
pressure: int
|
|
||||||
humidity: int
|
|
||||||
|
|
||||||
|
|
||||||
class GismeteoApi:
|
|
||||||
BASE_URL = "https://www.gismeteo.ru"
|
BASE_URL = "https://www.gismeteo.ru"
|
||||||
|
|
||||||
async def _request(self, endpoint: str) -> str:
|
async def _request(self, endpoint: str) -> str:
|
||||||
@@ -30,18 +21,25 @@ class GismeteoApi:
|
|||||||
async with session.request("GET", url) as response:
|
async with session.request("GET", url) as response:
|
||||||
return await response.text()
|
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]] = []
|
result: List[Dict[str, Any]] = []
|
||||||
soup = BeautifulSoup(data, features="html.parser")
|
soup = BeautifulSoup(data, features="html.parser")
|
||||||
|
location = LOCATION_PARSER.parse_location(data)
|
||||||
widget = ONE_DAY_PARSER.parse_widget(soup)
|
widget = ONE_DAY_PARSER.parse_widget(soup)
|
||||||
for parser in ROW_PARSERS:
|
for parser in ROW_PARSERS:
|
||||||
for index, value in enumerate(parser.parse_row(widget)):
|
for index, value in enumerate(parser.parse_row(widget)):
|
||||||
while len(result) < index + 1:
|
while len(result) < index + 1:
|
||||||
result.append({})
|
result.append({})
|
||||||
result[index][parser.KEY] = value
|
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)
|
location = LOCATION_BUNDLE.parse(location_id)
|
||||||
data = await self._request(f"weather-{location}/{dateutil.dump(date)}")
|
data = await self._request(f"weather-{location}/{datehelp.dump(date)}")
|
||||||
return self._parse_oneday(data)
|
return self._parse_oneday(date, data)
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -4,7 +4,6 @@ from bs4 import Tag
|
|||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class WidgetParser:
|
class WidgetParser:
|
||||||
def parse_widget(self, tag: Tag) -> Tag:
|
def parse_widget(self, tag: Tag) -> Tag:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import dateparser
|
from gismeteo.api import WeatherResponse
|
||||||
|
|
||||||
from gismeteo.api import WeatherValue
|
|
||||||
|
|
||||||
|
|
||||||
class MockData:
|
class MockData:
|
||||||
@@ -14,12 +11,9 @@ class MockData:
|
|||||||
return (Path(__file__).parent / "data/weather.html").read_text()
|
return (Path(__file__).parent / "data/weather.html").read_text()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def values(self) -> List[WeatherValue]:
|
def response(self) -> WeatherResponse:
|
||||||
data = json.loads((Path(__file__).parent / "data/weather.json").read_text())
|
data = json.loads((Path(__file__).parent / "data/weather.json").read_text())
|
||||||
return [
|
return WeatherResponse(**data)
|
||||||
WeatherValue(**{**item, "date": dateparser.parse(item["date"])})
|
|
||||||
for item in data
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
MOCK_DATA = MockData()
|
MOCK_DATA = MockData()
|
||||||
|
|||||||
@@ -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}]}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Dict, Iterable, List
|
import re
|
||||||
|
from typing import Dict, Iterable, List, Optional
|
||||||
|
|
||||||
import dateparser
|
import dateparser
|
||||||
from bs4 import Tag
|
from bs4 import Tag
|
||||||
@@ -9,6 +10,19 @@ from .core import BaseWidgetParser, RowParser
|
|||||||
ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items")
|
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]):
|
class DateParser(RowParser[datetime.datetime]):
|
||||||
KEY = "date"
|
KEY = "date"
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
|
||||||
2
poetry.lock
generated
2
poetry.lock
generated
@@ -1795,4 +1795,4 @@ multidict = ">=4.0"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "6940ed3fa5467b63dde6410fcb427cbcf93c4366f967e07cf24601d51b43a28f"
|
content-hash = "bc04729da7680c2e078b4abd6e402186b54a938ef5cee388b0b2c462f6c167f1"
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "gismeteo-api"
|
name = "weather"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["shmyga <shmyga.z@gmail.com>"]
|
authors = ["shmyga <shmyga.z@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
packages = [{ include = "gismeteo" }]
|
packages = [{ include = "weather" }, { include = "gismeteo" }]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.12"
|
python = "^3.12"
|
||||||
aiohttp = "^3.9.5"
|
aiohttp = "^3.9.5"
|
||||||
beautifulsoup4 = "^4.12.3"
|
beautifulsoup4 = "^4.12.3"
|
||||||
dateparser = "^1.2.0"
|
dateparser = "^1.2.0"
|
||||||
|
pydantic = "^2.8.2"
|
||||||
|
|
||||||
[tool.poetry.group.app.dependencies]
|
[tool.poetry.group.app.dependencies]
|
||||||
fastapi = "^0.111.1"
|
fastapi = "^0.111.1"
|
||||||
@@ -30,7 +31,7 @@ requires = ["poetry-core"]
|
|||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
app = 'gismeteo.app:run'
|
app = 'weather.app:run'
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-p no:warnings"
|
addopts = "-p no:warnings"
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ async def test_api(gismeteo_api: GismeteoApi):
|
|||||||
result = await gismeteo_api.get_day(
|
result = await gismeteo_api.get_day(
|
||||||
"zmiyevka", datetime.date.today() + datetime.timedelta(days=1)
|
"zmiyevka", datetime.date.today() + datetime.timedelta(days=1)
|
||||||
)
|
)
|
||||||
assert len(result) == 8
|
assert len(result.values) == 8
|
||||||
15
weather/api.py
Normal file
15
weather/api.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from .model import WeatherResponse
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherApi:
|
||||||
|
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_API: WeatherApi = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_api() -> WeatherApi:
|
||||||
|
return DEFAULT_API
|
||||||
27
weather/app.py
Normal file
27
weather/app.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import locale
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from gismeteo.api import GismeteoApi
|
||||||
|
|
||||||
|
from . import api as _api
|
||||||
|
from .route import api, doc, view
|
||||||
|
|
||||||
|
_api.DEFAULT_API = GismeteoApi()
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Weather",
|
||||||
|
docs_url=None,
|
||||||
|
redoc_url=None,
|
||||||
|
)
|
||||||
|
doc.mount(app)
|
||||||
|
api.mount(app)
|
||||||
|
view.mount(app)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
uvicorn.run("weather.app:app", host="0.0.0.0", port=8000, reload="DEBUG" in environ)
|
||||||
22
weather/model.py
Normal file
22
weather/model.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherValue(BaseModel):
|
||||||
|
date: datetime.datetime
|
||||||
|
cloudness: str
|
||||||
|
temperature: int
|
||||||
|
wind_speed: int
|
||||||
|
wind_gust: int
|
||||||
|
wind_direction: str
|
||||||
|
precipitation: float
|
||||||
|
pressure: int
|
||||||
|
humidity: int
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherResponse(BaseModel):
|
||||||
|
location: str
|
||||||
|
date: datetime.date
|
||||||
|
period: str
|
||||||
|
values: list[WeatherValue]
|
||||||
12
weather/route/api.py
Normal file
12
weather/route/api.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from weather.api import get_api
|
||||||
|
from weather.model import WeatherResponse
|
||||||
|
|
||||||
|
|
||||||
|
def mount(app: FastAPI):
|
||||||
|
@app.get("/api/weather/{location}/{date}")
|
||||||
|
async def get_weather(location: str, date: datetime.date) -> WeatherResponse:
|
||||||
|
return await get_api().get_day(location, date)
|
||||||
@@ -6,9 +6,8 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from gismeteo import dateutil
|
|
||||||
from gismeteo.api import GismeteoApi
|
|
||||||
from gismeteo.mock import MOCK_DATA
|
from gismeteo.mock import MOCK_DATA
|
||||||
|
from weather.api import get_api
|
||||||
|
|
||||||
from .filters import cloudness_icon, wind_direction_icon
|
from .filters import cloudness_icon, wind_direction_icon
|
||||||
|
|
||||||
@@ -20,24 +19,21 @@ def mount(app: FastAPI):
|
|||||||
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
||||||
templates.env.filters["cloudness_icon"] = cloudness_icon
|
templates.env.filters["cloudness_icon"] = cloudness_icon
|
||||||
|
|
||||||
@app.get("/weather/{location}")
|
@app.get("/weather/{location}", response_class=RedirectResponse)
|
||||||
async def get_weather_base(location: str):
|
async def get_weather_default(location: str):
|
||||||
return RedirectResponse(f"{location}/today")
|
return RedirectResponse(f"{location}/{datetime.date.today()}")
|
||||||
|
|
||||||
@app.get("/weather/{location}/{date}", response_class=HTMLResponse)
|
@app.get("/weather/{location}/{date}", response_class=HTMLResponse)
|
||||||
async def get_weather(request: Request, location: str, date: str):
|
async def get_weather(request: Request, location: str, date: datetime.date):
|
||||||
if date == "mock":
|
if date == "mock":
|
||||||
values = MOCK_DATA.values
|
response = MOCK_DATA.response
|
||||||
else:
|
else:
|
||||||
api = GismeteoApi()
|
response = await get_api().get_day(location, date)
|
||||||
values = await api.get_day(location, dateutil.parse(date))
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="weather.html",
|
name="weather.html",
|
||||||
context={
|
context={
|
||||||
"datetime": datetime,
|
"datetime": datetime,
|
||||||
"location": location,
|
"response": response,
|
||||||
"date": dateutil.parse(date),
|
|
||||||
"values": values,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -24,4 +24,5 @@ def cloudness_icon(cloudness: str) -> str:
|
|||||||
"Облачно, небольшой дождь, гроза": "⛈️",
|
"Облачно, небольшой дождь, гроза": "⛈️",
|
||||||
"Малооблачно, дождь": "🌦️",
|
"Малооблачно, дождь": "🌦️",
|
||||||
"Пасмурно, небольшой дождь": "🌧️",
|
"Пасмурно, небольшой дождь": "🌧️",
|
||||||
|
"Облачно, дождь, гроза": "⛈️",
|
||||||
}.get(cloudness, cloudness)
|
}.get(cloudness, cloudness)
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
0
weather/route/view/static/index.js
Normal file
0
weather/route/view/static/index.js
Normal file
@@ -37,6 +37,7 @@ td {
|
|||||||
.header {
|
.header {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
padding-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
content="width=device-width, initial-scale=1.0">
|
content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible"
|
<meta http-equiv="X-UA-Compatible"
|
||||||
content="ie=edge">
|
content="ie=edge">
|
||||||
<title>Weather - {{date.strftime('%a, %d %B %Y')}}</title>
|
<title>Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</title>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="/static/style.css">
|
href="/static/style.css">
|
||||||
<link rel="icon"
|
<link rel="icon"
|
||||||
@@ -17,17 +17,17 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h3>
|
<h3>
|
||||||
<a class="button {{'disabled' if date == datetime.date.today() else ''}}"
|
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
||||||
href="{{date - datetime.timedelta(days=1)}}">🡨</a>
|
href="{{response.date - datetime.timedelta(days=1)}}">🡨</a>
|
||||||
<span>{{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="{{date + datetime.timedelta(days=1)}}">🡪</a>
|
href="{{response.date + datetime.timedelta(days=1)}}">🡪</a>
|
||||||
</h3>
|
</h3>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<!-- date -->
|
<!-- date -->
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in values %}
|
{% for value in response.values %}
|
||||||
<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>
|
||||||
@@ -36,13 +36,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- cloudness -->
|
<!-- cloudness -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Облачность
|
Облачность
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in values %}
|
{% for value in response.values %}
|
||||||
<td class="cloudness">
|
<td class="cloudness">
|
||||||
<span class="icon">{{value.cloudness | cloudness_icon}}</span>
|
<span class="icon">{{value.cloudness | cloudness_icon}}</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -50,13 +50,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- temperature -->
|
<!-- temperature -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Температура, °C
|
Температура, °C
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in values %}
|
{% for value in response.values %}
|
||||||
<td class="temperature {{'positive' if value.temperature > 0 else 'negative'}}"
|
<td class="temperature {{'positive' if value.temperature > 0 else 'negative'}}"
|
||||||
style="background-color: rgba(255, 128, 128, {{(value.temperature - 10) * 0.015}});">
|
style="background-color: rgba(255, 128, 128, {{(value.temperature - 10) * 0.015}});">
|
||||||
<span class="value">{{value.temperature}}</span>
|
<span class="value">{{value.temperature}}</span>
|
||||||
@@ -65,13 +65,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- wind_direction -->
|
<!-- wind_direction -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Направление ветра
|
Направление ветра
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in values %}
|
{% for value in response.values %}
|
||||||
<td class="wind">
|
<td class="wind">
|
||||||
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
|
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
|
||||||
<span class="direction">{{value.wind_direction}}</span>
|
<span class="direction">{{value.wind_direction}}</span>
|
||||||
@@ -80,13 +80,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- wind_speed -->
|
<!-- wind_speed -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Скорость ветра, м/с
|
Скорость ветра, м/с
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in values %}
|
{% for value in response.values %}
|
||||||
<td class="wind"
|
<td class="wind"
|
||||||
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});">
|
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});">
|
||||||
<span class="speed">{{value.wind_speed}}</span>
|
<span class="speed">{{value.wind_speed}}</span>
|
||||||
@@ -100,13 +100,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- precipitation -->
|
<!-- precipitation -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Осадки, мм
|
Осадки, мм
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in values %}
|
{% for value in response.values %}
|
||||||
<td class="precipitation"
|
<td class="precipitation"
|
||||||
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
|
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
|
||||||
<span class="value">{{value.precipitation or ' '}}</span>
|
<span class="value">{{value.precipitation or ' '}}</span>
|
||||||
@@ -115,13 +115,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- pressure -->
|
<!-- pressure -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Давление, мм рт. ст.
|
Давление, мм рт. ст.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in values %}
|
{% for value in response.values %}
|
||||||
<td class="pressure"
|
<td class="pressure"
|
||||||
style="background-color: rgba(128, 0, 255, {{(value.pressure - 720) * 0.008}});">
|
style="background-color: rgba(128, 0, 255, {{(value.pressure - 720) * 0.008}});">
|
||||||
<span class="value">{{value.pressure}}</span>
|
<span class="value">{{value.pressure}}</span>
|
||||||
@@ -130,13 +130,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- humidity -->
|
<!-- humidity -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Влажность, %
|
Влажность, %
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in values %}
|
{% for value in response.values %}
|
||||||
<td class="humidity"
|
<td class="humidity"
|
||||||
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
|
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
|
||||||
<span class="value">{{value.humidity}}</span>
|
<span class="value">{{value.humidity}}</span>
|
||||||
Reference in New Issue
Block a user