style: format code
This commit is contained in:
@@ -34,6 +34,8 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "uvicorn",
|
"module": "uvicorn",
|
||||||
"args": ["gallery.main:app", "--reload", "--log-config", "gallery/logging.yaml"],
|
"args": ["gallery.main:app", "--reload", "--log-config", "gallery/logging.yaml"],
|
||||||
|
"justMyCode": true,
|
||||||
|
"consoleTitle": "gallery:app",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gallery:static",
|
"name": "gallery:static",
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"command": "npm run dev",
|
"command": "npm run dev",
|
||||||
|
"consoleTitle": "gallery:static",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ def mount(app: FastAPI):
|
|||||||
return await schedule_api.get_channels()
|
return await schedule_api.get_channels()
|
||||||
|
|
||||||
@app.get("/api/schedule/{channel}/{date}", tags=["API"])
|
@app.get("/api/schedule/{channel}/{date}", tags=["API"])
|
||||||
async def get_api_schedule_channel_schedule(
|
async def get_api_schedule_channel_schedule(request: AppRequest, channel: str, date: datetime.date) -> Schedule:
|
||||||
request: AppRequest, channel: str, date: datetime.date
|
|
||||||
) -> Schedule:
|
|
||||||
schedule_api = request.app.state.api.schedule
|
schedule_api = request.app.state.api.schedule
|
||||||
return await schedule_api.get_channel_schedule(ChannelId(channel), date)
|
return await schedule_api.get_channel_schedule(ChannelId(channel), date)
|
||||||
|
|||||||
@@ -8,22 +8,16 @@ from gallery.sketch.weather.model import Location, WeatherResponse
|
|||||||
|
|
||||||
def mount(app: FastAPI):
|
def mount(app: FastAPI):
|
||||||
@app.get("/api/weather/locations", tags=["API"])
|
@app.get("/api/weather/locations", tags=["API"])
|
||||||
async def get_api_weather_locations(
|
async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]:
|
||||||
request: AppRequest, query: str
|
|
||||||
) -> list[Location]:
|
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
return await weather_api.find_locations(query)
|
return await weather_api.find_locations(query)
|
||||||
|
|
||||||
@app.get("/api/weather/{location}/day/{date}", tags=["API"])
|
@app.get("/api/weather/{location}/day/{date}", tags=["API"])
|
||||||
async def get_api_weather_day(
|
async def get_api_weather_day(request: AppRequest, location: str, date: datetime.date) -> WeatherResponse:
|
||||||
request: AppRequest, location: str, date: datetime.date
|
|
||||||
) -> WeatherResponse:
|
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
return await weather_api.get_day(location, date)
|
return await weather_api.get_day(location, date)
|
||||||
|
|
||||||
@app.get("/api/weather/{location}/days/{days}", tags=["API"])
|
@app.get("/api/weather/{location}/days/{days}", tags=["API"])
|
||||||
async def get_api_weather_days(
|
async def get_api_weather_days(request: AppRequest, location: str, days: int) -> WeatherResponse:
|
||||||
request: AppRequest, location: str, days: int
|
|
||||||
) -> WeatherResponse:
|
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
return await weather_api.get_days(location, days)
|
return await weather_api.get_days(location, days)
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ from typing import NamedTuple
|
|||||||
|
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
|
|
||||||
from gallery.version import __version__
|
from ..common.utils.template import build_templates
|
||||||
|
|
||||||
from ..translation import _
|
|
||||||
|
|
||||||
|
|
||||||
class Section(NamedTuple):
|
class Section(NamedTuple):
|
||||||
@@ -25,8 +22,7 @@ base_dir = Path(__file__).parent
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
templates = Jinja2Templates(directory=base_dir / "templates")
|
templates = build_templates()
|
||||||
templates.env.globals.update({"_": _})
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
@@ -35,7 +31,6 @@ async def get_section_list(request: Request):
|
|||||||
request=request,
|
request=request,
|
||||||
name="root_index.html",
|
name="root_index.html",
|
||||||
context={
|
context={
|
||||||
"version": __version__,
|
|
||||||
"sections": SECTIONS,
|
"sections": SECTIONS,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
Created by shmyga · © 2026
|
Created by shmyga · © 2026
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
{# <script>
|
||||||
(function () {
|
(function () {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const widget = params.get('widget') || window.location.hostname.startsWith('weather');
|
const widget = params.get('widget') || window.location.hostname.startsWith('weather');
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
document.body.classList.add('widget');
|
document.body.classList.add('widget');
|
||||||
}
|
}
|
||||||
}());
|
}());
|
||||||
</script>
|
</script> #}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
0
gallery/easel/route/view/common/utils/__init__.py
Normal file
0
gallery/easel/route/view/common/utils/__init__.py
Normal file
25
gallery/easel/route/view/common/utils/template.py
Normal file
25
gallery/easel/route/view/common/utils/template.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from babel.dates import format_date
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from gallery.version import __version__
|
||||||
|
|
||||||
|
from ...translation import _
|
||||||
|
|
||||||
|
|
||||||
|
def build_templates(templates_dir: Path | None = None, filters: dict | None = None) -> Jinja2Templates:
|
||||||
|
directory = [Path(__file__).parent.parent / "templates"]
|
||||||
|
if templates_dir:
|
||||||
|
directory.append(templates_dir)
|
||||||
|
templates = Jinja2Templates(directory=directory)
|
||||||
|
templates.env.globals.update(
|
||||||
|
{
|
||||||
|
"_": _,
|
||||||
|
"version": __version__,
|
||||||
|
"format_date": format_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if filters:
|
||||||
|
templates.env.filters.update(filters)
|
||||||
|
return templates
|
||||||
@@ -1,34 +1,22 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from babel.dates import format_date
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
|
|
||||||
from gallery.easel.core import AppRequest
|
from gallery.easel.core import AppRequest
|
||||||
from gallery.sketch.schedule.catalog import BUNDLE
|
from gallery.sketch.schedule.catalog import BUNDLE
|
||||||
from gallery.version import __version__
|
|
||||||
|
|
||||||
from ..common.util import TagType, TagUtil
|
from ..common.utils.tag import TagType, TagUtil
|
||||||
from ..translation import _
|
from ..common.utils.template import build_templates
|
||||||
from .filters import timedelta_format
|
from .filters import timedelta_format
|
||||||
|
|
||||||
base_dir = Path(__file__).parent
|
templates = build_templates(
|
||||||
templates = Jinja2Templates(
|
Path(__file__).parent / "templates",
|
||||||
directory=[
|
|
||||||
base_dir.parent / "common/templates",
|
|
||||||
base_dir / "templates",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
templates.env.globals.update(
|
|
||||||
{
|
{
|
||||||
"_": _,
|
"timedelta_format": timedelta_format,
|
||||||
"version": __version__,
|
},
|
||||||
"format_date": format_date,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
templates.env.filters["timedelta_format"] = timedelta_format
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -42,7 +30,6 @@ async def get_schedule_list(request: AppRequest):
|
|||||||
request=request,
|
request=request,
|
||||||
name="index.html",
|
name="index.html",
|
||||||
context={
|
context={
|
||||||
"version": __version__,
|
|
||||||
"channels": channels_data,
|
"channels": channels_data,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -57,7 +44,6 @@ async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False):
|
|||||||
request=request,
|
request=request,
|
||||||
name="schedule.html",
|
name="schedule.html",
|
||||||
context={
|
context={
|
||||||
"version": __version__,
|
|
||||||
"tag_util": TagUtil,
|
"tag_util": TagUtil,
|
||||||
"datetime": datetime,
|
"datetime": datetime,
|
||||||
"response": results[0],
|
"response": results[0],
|
||||||
@@ -84,7 +70,6 @@ async def get_channel_tag(request: AppRequest, channel: str, tag: str):
|
|||||||
request=request,
|
request=request,
|
||||||
name="channel.html",
|
name="channel.html",
|
||||||
context={
|
context={
|
||||||
"version": __version__,
|
|
||||||
"tag_util": TagUtil,
|
"tag_util": TagUtil,
|
||||||
"datetime": datetime,
|
"datetime": datetime,
|
||||||
"response": response,
|
"response": response,
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ from fastapi import Cookie, Header, Request
|
|||||||
|
|
||||||
from gallery.util import root_path
|
from gallery.util import root_path
|
||||||
|
|
||||||
_translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = (
|
_translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = ContextVar("translation")
|
||||||
ContextVar("translation")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def set_language(
|
async def set_language(
|
||||||
@@ -19,9 +17,7 @@ async def set_language(
|
|||||||
lang = language or accept_language.split(",")[0].split("-")[0]
|
lang = language or accept_language.split(",")[0].split("-")[0]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
t = gettext.translation(
|
t = gettext.translation("messages", localedir=root_path / "locales", languages=[lang])
|
||||||
"messages", localedir=root_path / "locales", languages=[lang]
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
t = gettext.NullTranslations()
|
t = gettext.NullTranslations()
|
||||||
|
|
||||||
|
|||||||
@@ -3,33 +3,21 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from babel.dates import format_date
|
|
||||||
from gallery.easel.core import AppRequest
|
from gallery.easel.core import AppRequest
|
||||||
from gallery.sketch.weather.model import WeatherResponse
|
from gallery.sketch.weather.model import WeatherResponse
|
||||||
from gallery.version import __version__
|
|
||||||
|
|
||||||
from ..common.util import TagType, TagUtil
|
from ..common.utils.tag import TagType, TagUtil
|
||||||
from ..translation import _
|
from ..common.utils.template import build_templates
|
||||||
from .filters import cloudness_icon, wind_direction_icon
|
from .filters import cloudness_icon, wind_direction_icon
|
||||||
|
|
||||||
|
templates = build_templates(
|
||||||
base_dir = Path(__file__).parent
|
Path(__file__).parent / "templates",
|
||||||
templates = Jinja2Templates(
|
|
||||||
directory=[
|
|
||||||
base_dir.parent / "common/templates",
|
|
||||||
base_dir / "templates",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
templates.env.globals.update(
|
|
||||||
{
|
{
|
||||||
"_": _,
|
"wind_direction_icon": wind_direction_icon,
|
||||||
"version": __version__,
|
"cloudness_icon": cloudness_icon,
|
||||||
"format_date": format_date,
|
},
|
||||||
}
|
|
||||||
)
|
)
|
||||||
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
|
||||||
templates.env.filters["cloudness_icon"] = cloudness_icon
|
|
||||||
|
|
||||||
|
|
||||||
def build_weather_response(request: AppRequest, response: WeatherResponse):
|
def build_weather_response(request: AppRequest, response: WeatherResponse):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ 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]:
|
||||||
datetime_date_tag = tag.select_one(
|
datetime_date_tag = tag.select_one(".widget-row.widget-row-datetime-date > .row-item")
|
||||||
".widget-row.widget-row-datetime-date > .row-item"
|
|
||||||
)
|
|
||||||
if datetime_date_tag:
|
if datetime_date_tag:
|
||||||
date_str = datetime_date_tag.find(text=True, recursive=False).text
|
date_str = datetime_date_tag.find(text=True, recursive=False).text
|
||||||
date = dateparser.parse(date_str, languages=["ru"])
|
date = dateparser.parse(date_str, languages=["ru"])
|
||||||
@@ -108,21 +106,15 @@ class TemperatureParser(RowParser[list[int]]):
|
|||||||
KEY = "temperature"
|
KEY = "temperature"
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[list[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"):
|
||||||
".widget-row-chart[data-row=temperature-air] > .chart > .values > .value"
|
yield [int(value.attrs["value"]) for value in item.select("temperature-value")]
|
||||||
):
|
|
||||||
yield [
|
|
||||||
int(value.attrs["value"]) for value in item.select("temperature-value")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WindSpeedParser(RowParser[int]):
|
class WindSpeedParser(RowParser[int]):
|
||||||
KEY = "wind_speed"
|
KEY = "wind_speed"
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[int]:
|
def parse_row(self, tag: Tag) -> Iterable[int]:
|
||||||
for item in tag.select(
|
for item in tag.select(".widget-row-wind > .row-item > .wind-speed > speed-value"):
|
||||||
".widget-row-wind > .row-item > .wind-speed > speed-value"
|
|
||||||
):
|
|
||||||
yield int(item.attrs["value"])
|
yield int(item.attrs["value"])
|
||||||
|
|
||||||
|
|
||||||
@@ -152,22 +144,16 @@ class WindDirectionParser(RowParser[WindDirection]):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[float]:
|
def parse_row(self, tag: Tag) -> Iterable[float]:
|
||||||
for item in tag.select(
|
for item in tag.select(".widget-row-wind > .row-item > .wind-speed > .wind-direction"):
|
||||||
".widget-row-wind > .row-item > .wind-speed > .wind-direction"
|
|
||||||
):
|
|
||||||
wind_direction_str = item.text.lower().strip()
|
wind_direction_str = item.text.lower().strip()
|
||||||
yield WindDirectionDeg.from_direction(
|
yield WindDirectionDeg.from_direction(self.WIND_DIRECTION_MAP[wind_direction_str]).value
|
||||||
self.WIND_DIRECTION_MAP[wind_direction_str]
|
|
||||||
).value
|
|
||||||
|
|
||||||
|
|
||||||
class PrecipitationParser(RowParser[float]):
|
class PrecipitationParser(RowParser[float]):
|
||||||
KEY = "precipitation"
|
KEY = "precipitation"
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[float]:
|
def parse_row(self, tag: Tag) -> Iterable[float]:
|
||||||
for item in tag.select(
|
for item in tag.select(".widget-row[data-row=precipitation-bars] > .row-item > .item-unit"):
|
||||||
".widget-row[data-row=precipitation-bars] > .row-item > .item-unit"
|
|
||||||
):
|
|
||||||
yield float(item.text.replace(",", "."))
|
yield float(item.text.replace(",", "."))
|
||||||
|
|
||||||
|
|
||||||
@@ -175,9 +161,7 @@ class PressureParser(RowParser[list[int]]):
|
|||||||
KEY = "pressure"
|
KEY = "pressure"
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[list[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"):
|
||||||
".widget-row-chart[data-row=pressure] > .chart > .values > .value"
|
|
||||||
):
|
|
||||||
yield [int(value.attrs["value"]) for value in item.select("pressure-value")]
|
yield [int(value.attrs["value"]) for value in item.select("pressure-value")]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,32 +25,20 @@ class MatchTvApi(ScheduleApi):
|
|||||||
ChannelId.MATCH_STRANA,
|
ChannelId.MATCH_STRANA,
|
||||||
]
|
]
|
||||||
|
|
||||||
async def get_channel_schedule(
|
async def get_channel_schedule(self, channel_id: ChannelId, date: datetime.date) -> Schedule:
|
||||||
self, channel_id: ChannelId, date: datetime.date
|
|
||||||
) -> Schedule:
|
|
||||||
endpoint = f"tvguide/{channel_id}?date={date:%Y%m%d}"
|
endpoint = f"tvguide/{channel_id}?date={date:%Y%m%d}"
|
||||||
data = await self.SOURCE.request(endpoint)
|
data = await self.SOURCE.request(endpoint)
|
||||||
soup = BeautifulSoup(data, features="html.parser")
|
soup = BeautifulSoup(data, features="html.parser")
|
||||||
values = []
|
values = []
|
||||||
channel_name = (
|
channel_name = soup.select_one(".p-tv-guide-header__title").text.replace("Телепрограмма ", "").strip()
|
||||||
soup.select_one(".p-tv-guide-header__title")
|
current_day = datetime.datetime.combine(date.today(), datetime.datetime.min.time())
|
||||||
.text.replace("Телепрограмма ", "")
|
|
||||||
.strip()
|
|
||||||
)
|
|
||||||
current_day = datetime.datetime.combine(
|
|
||||||
date.today(), datetime.datetime.min.time()
|
|
||||||
)
|
|
||||||
end = current_day + datetime.timedelta(days=1, hours=6)
|
end = current_day + datetime.timedelta(days=1, hours=6)
|
||||||
prev_value: ScheduleValue | None = None
|
prev_value: ScheduleValue | None = None
|
||||||
for item in soup.select(
|
for item in soup.select(
|
||||||
".p-tv-guide-schedule-channel-carcass__transmissions .p-tv-guide-schedule-channel-transmission"
|
".p-tv-guide-schedule-channel-carcass__transmissions .p-tv-guide-schedule-channel-transmission"
|
||||||
):
|
):
|
||||||
title = item.select_one(
|
title = item.select_one(".p-tv-guide-schedule-channel-transmission__title").text.strip()
|
||||||
".p-tv-guide-schedule-channel-transmission__title"
|
time_str = item.select_one(".p-tv-guide-schedule-channel-transmission__time-block").text.strip()
|
||||||
).text.strip()
|
|
||||||
time_str = item.select_one(
|
|
||||||
".p-tv-guide-schedule-channel-transmission__time-block"
|
|
||||||
).text.strip()
|
|
||||||
hours, minutes = map(int, time_str.split(":"))
|
hours, minutes = map(int, time_str.split(":"))
|
||||||
item_date = current_day.replace(hour=hours, minute=minutes)
|
item_date = current_day.replace(hour=hours, minute=minutes)
|
||||||
if prev_value is not None and item_date.hour < prev_value.start.hour:
|
if prev_value is not None and item_date.hour < prev_value.start.hour:
|
||||||
@@ -62,6 +50,4 @@ class MatchTvApi(ScheduleApi):
|
|||||||
if prev_value is not None:
|
if prev_value is not None:
|
||||||
prev_value.end = item_date
|
prev_value.end = item_date
|
||||||
prev_value = value
|
prev_value = value
|
||||||
return Schedule(
|
return Schedule(channel=Channel(id=channel_id, name=channel_name), date=date, values=values)
|
||||||
channel=Channel(id=channel_id, name=channel_name), date=date, values=values
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -55,10 +55,7 @@ class OpenWeatherApi(WeatherApi):
|
|||||||
value = FORECAST_ITEM_PARSER.parse(item)
|
value = FORECAST_ITEM_PARSER.parse(item)
|
||||||
item_date = value.date.replace(hour=0, minute=0)
|
item_date = value.date.replace(hour=0, minute=0)
|
||||||
values_by_date[item_date].append(value)
|
values_by_date[item_date].append(value)
|
||||||
values = [
|
values = [merge_weather_values(date, values) for date, values in values_by_date.items()]
|
||||||
merge_weather_values(date, values)
|
|
||||||
for date, values in values_by_date.items()
|
|
||||||
]
|
|
||||||
return WeatherResponse(
|
return WeatherResponse(
|
||||||
location=location_id,
|
location=location_id,
|
||||||
date=datetime.date.today(),
|
date=datetime.date.today(),
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from gallery.sketch.mock import MockData
|
|
||||||
|
|
||||||
OPENWEATHER_MOCK_DATA = MockData(Path(__file__).parent / "data")
|
|
||||||
@@ -75,9 +75,7 @@ class OpenWeather:
|
|||||||
self._source = ApiSource(self.BASE_URL)
|
self._source = ApiSource(self.BASE_URL)
|
||||||
|
|
||||||
async def get_forecast(self, lat: float, lon: float) -> Forecast:
|
async def get_forecast(self, lat: float, lon: float) -> Forecast:
|
||||||
endpoint = (
|
endpoint = f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric"
|
||||||
f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric"
|
|
||||||
)
|
|
||||||
response = await self._source.request(endpoint)
|
response = await self._source.request(endpoint)
|
||||||
response_data = json.loads(response)
|
response_data = json.loads(response)
|
||||||
return Forecast(**response_data)
|
return Forecast(**response_data)
|
||||||
|
|||||||
@@ -24,11 +24,7 @@ class ForecastItemParser:
|
|||||||
|
|
||||||
def parse(self, item: ForecastItem) -> WeatherValue:
|
def parse(self, item: ForecastItem) -> WeatherValue:
|
||||||
item_date = datetime.datetime.fromtimestamp(item.dt, datetime.UTC)
|
item_date = datetime.datetime.fromtimestamp(item.dt, datetime.UTC)
|
||||||
item_date = (
|
item_date = item_date.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None).replace(tzinfo=None)
|
||||||
item_date.replace(tzinfo=datetime.timezone.utc)
|
|
||||||
.astimezone(tz=None)
|
|
||||||
.replace(tzinfo=None)
|
|
||||||
)
|
|
||||||
value = build_weather_value(item_date)
|
value = build_weather_value(item_date)
|
||||||
# TODO parse temperature interval flag
|
# TODO parse temperature interval flag
|
||||||
value.temperature = [round(item.main.temp)]
|
value.temperature = [round(item.main.temp)]
|
||||||
@@ -38,12 +34,8 @@ class ForecastItemParser:
|
|||||||
value.wind_speed = round(item.wind.speed)
|
value.wind_speed = round(item.wind.speed)
|
||||||
value.wind_gust = round(item.wind.gust)
|
value.wind_gust = round(item.wind.gust)
|
||||||
value.wind_direction = item.wind.deg
|
value.wind_direction = item.wind.deg
|
||||||
value.sky.cloudness = self.CLOUDNESS_MAP.get(
|
value.sky.cloudness = self.CLOUDNESS_MAP.get(item.weather[0].description, Cloudness.CLEAR)
|
||||||
item.weather[0].description, Cloudness.CLEAR
|
value.sky.precipitation = self.PRECIPITATION_MAP.get(item.weather[0].description, Precipitation.NO)
|
||||||
)
|
|
||||||
value.sky.precipitation = self.PRECIPITATION_MAP.get(
|
|
||||||
item.weather[0].description, Precipitation.NO
|
|
||||||
)
|
|
||||||
if item.rain:
|
if item.rain:
|
||||||
value.precipitation = round(item.rain.interval_3h, 1)
|
value.precipitation = round(item.rain.interval_3h, 1)
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ class YandexTvApi(ScheduleApi):
|
|||||||
async def get_channels(self) -> list[ChannelId]:
|
async def get_channels(self) -> list[ChannelId]:
|
||||||
return list(CHANNELS_MAP.keys())
|
return list(CHANNELS_MAP.keys())
|
||||||
|
|
||||||
async def get_channel_schedule(
|
async def get_channel_schedule(self, channel_id: ChannelId, date: datetime.date) -> Schedule:
|
||||||
self, channel_id: ChannelId, date: datetime.date
|
|
||||||
) -> Schedule:
|
|
||||||
endpoint = f"channel/{CHANNELS_MAP[channel_id]}?date={date:%Y-%m-%d}"
|
endpoint = f"channel/{CHANNELS_MAP[channel_id]}?date={date:%Y-%m-%d}"
|
||||||
data = await self.SOURCE.request(endpoint)
|
data = await self.SOURCE.request(endpoint)
|
||||||
soup = BeautifulSoup(data, features="html.parser")
|
soup = BeautifulSoup(data, features="html.parser")
|
||||||
@@ -70,9 +68,7 @@ class YandexTvApi(ScheduleApi):
|
|||||||
raise RuntimeError("Captcha")
|
raise RuntimeError("Captcha")
|
||||||
values = []
|
values = []
|
||||||
channel_name = soup.select_one(".channel-header__text").text.strip()
|
channel_name = soup.select_one(".channel-header__text").text.strip()
|
||||||
current_day = datetime.datetime.combine(
|
current_day = datetime.datetime.combine(date.today(), datetime.datetime.min.time())
|
||||||
date.today(), datetime.datetime.min.time()
|
|
||||||
)
|
|
||||||
end = current_day + datetime.timedelta(days=1, hours=6)
|
end = current_day + datetime.timedelta(days=1, hours=6)
|
||||||
prev_value: ScheduleValue | None = None
|
prev_value: ScheduleValue | None = None
|
||||||
for item in soup.select(".channel-schedule .channel-schedule__event"):
|
for item in soup.select(".channel-schedule .channel-schedule__event"):
|
||||||
@@ -89,6 +85,4 @@ class YandexTvApi(ScheduleApi):
|
|||||||
if prev_value is not None:
|
if prev_value is not None:
|
||||||
prev_value.end = item_date
|
prev_value.end = item_date
|
||||||
prev_value = value
|
prev_value = value
|
||||||
return Schedule(
|
return Schedule(channel=Channel(id=channel_id, name=channel_name), date=date, values=values)
|
||||||
channel=Channel(id=channel_id, name=channel_name), date=date, values=values
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -11,18 +11,14 @@ class ScheduleApi(Api):
|
|||||||
async def get_channels(self) -> list[ChannelId]:
|
async def get_channels(self) -> list[ChannelId]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def get_channel_schedule(
|
async def get_channel_schedule(self, channel_id: ChannelId, date: datetime.date) -> Schedule:
|
||||||
self, channel_id: ChannelId, date: datetime.date
|
|
||||||
) -> Schedule:
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
|
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
|
||||||
channels = await self.get_channels()
|
channels = await self.get_channels()
|
||||||
results = []
|
results = []
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
results.append(
|
results.append(await self.get_channel_schedule(channel_id=channel, date=date))
|
||||||
await self.get_channel_schedule(channel_id=channel, date=date)
|
|
||||||
)
|
|
||||||
if self.INTERVAL > 0:
|
if self.INTERVAL > 0:
|
||||||
await asyncio.sleep(self.INTERVAL)
|
await asyncio.sleep(self.INTERVAL)
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -27,15 +27,11 @@ class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
|
|||||||
),
|
),
|
||||||
**CACHE_PRESET._asdict(),
|
**CACHE_PRESET._asdict(),
|
||||||
)
|
)
|
||||||
async def get_channel_schedule(
|
async def get_channel_schedule(self, channel_id: ChannelId, date: datetime.date) -> Schedule:
|
||||||
self, channel_id: ChannelId, date: datetime.date
|
|
||||||
) -> Schedule:
|
|
||||||
return await self._api.get_channel_schedule(channel_id, date)
|
return await self._api.get_channel_schedule(channel_id, date)
|
||||||
|
|
||||||
@cached(
|
@cached(
|
||||||
key_builder=lambda fun, self, date: (
|
key_builder=lambda fun, self, date: (f"api.{self.CACHE_KEY}.{self.provider}.all.{date}"),
|
||||||
f"api.{self.CACHE_KEY}.{self.provider}.all.{date}"
|
|
||||||
),
|
|
||||||
**CACHE_PRESET._asdict(),
|
**CACHE_PRESET._asdict(),
|
||||||
)
|
)
|
||||||
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
|
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ logger = logging.getLogger("source")
|
|||||||
|
|
||||||
class ApiSource:
|
class ApiSource:
|
||||||
DEFAULT_USER_AGENT = (
|
DEFAULT_USER_AGENT = (
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) "
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
||||||
"Chrome/126.0.0.0 Safari/537.36"
|
|
||||||
)
|
)
|
||||||
DEFAULT_TIMEOUT = 30.0
|
DEFAULT_TIMEOUT = 30.0
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ def build_weather_value(date: datetime.datetime) -> WeatherValue:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def merge_weather_values(
|
def merge_weather_values(date: datetime.datetime, values: list[WeatherValue]) -> WeatherValue:
|
||||||
date: datetime.datetime, values: list[WeatherValue]
|
|
||||||
) -> WeatherValue:
|
|
||||||
result = build_weather_value(date)
|
result = build_weather_value(date)
|
||||||
temperatures = []
|
temperatures = []
|
||||||
pressures = []
|
pressures = []
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
__version__ = "0.2.2"
|
__version__ = "0.2.2"
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
__version__ = tomllib.loads((Path(__file__).parent.parent / "pyproject.toml").read_text())["tool"]["poetry"]["version"]
|
||||||
|
|||||||
@@ -35,9 +35,13 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
gallery = "gallery.main:run"
|
gallery = "gallery.main:run"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-p no:warnings"
|
addopts = "-p no:warnings"
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|
||||||
[tool.poetry_bumpversion.file."gallery/version.py"]
|
|
||||||
8
scripts/format
Executable file
8
scripts/format
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname $(dirname "$0"))" || exit
|
||||||
|
|
||||||
|
TARGET="gallery"
|
||||||
|
|
||||||
|
poetry run isort $TARGET
|
||||||
|
poetry run black $TARGET -q
|
||||||
@@ -2,4 +2,8 @@
|
|||||||
set -e
|
set -e
|
||||||
cd "$(dirname $(dirname "$0"))" || exit
|
cd "$(dirname $(dirname "$0"))" || exit
|
||||||
|
|
||||||
poetry run pylint gallery
|
TARGET="gallery"
|
||||||
|
|
||||||
|
poetry run pylint $TARGET
|
||||||
|
poetry run isort $TARGET --check-only
|
||||||
|
poetry run black $TARGET -q --check --diff
|
||||||
|
|||||||
15
scripts/setup
Executable file
15
scripts/setup
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname $(dirname "$0"))" || exit
|
||||||
|
|
||||||
|
PYTHON_VERSION=3.12
|
||||||
|
poetry env use ${PYTHON_VERSION}
|
||||||
|
poetry install
|
||||||
|
|
||||||
|
cd static || exit
|
||||||
|
|
||||||
|
if [[ -f $HOME/.nvm/nvm.sh ]]; then
|
||||||
|
source "$HOME/.nvm/nvm.sh"
|
||||||
|
nvm use
|
||||||
|
fi
|
||||||
|
npm ci
|
||||||
Reference in New Issue
Block a user