diff --git a/gallery/easel/route/api/weather.py b/gallery/easel/route/api/weather.py index c763927..4c7e1f7 100644 --- a/gallery/easel/route/api/weather.py +++ b/gallery/easel/route/api/weather.py @@ -8,6 +8,11 @@ from gallery.sketch.weather.model import Location, WeatherResponse router = APIRouter(prefix="/weather") +@router.get("/providers") +async def get_api_weather_providers(request: AppRequest) -> list[str]: + return request.app.state.api.get_api_providers("weather") + + @router.get("/locations") async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]: weather_api = request.app.state.api.weather diff --git a/gallery/easel/route/view/common/utils/template.py b/gallery/easel/route/view/common/utils/template.py index c12f377..df85b6a 100644 --- a/gallery/easel/route/view/common/utils/template.py +++ b/gallery/easel/route/view/common/utils/template.py @@ -1,15 +1,19 @@ import datetime +import typing from pathlib import Path from babel.dates import format_date from fastapi import Request from fastapi.templating import Jinja2Templates +from gallery.easel.core import AppRequest from gallery.version import __version__ from ...translation import _ from .tag import TagUtil +ContextProcessor = typing.Callable[[AppRequest], dict[str, typing.Any]] + def is_widget(request: Request) -> bool: return (request.url.hostname and request.url.hostname.startswith("weather")) or ( @@ -17,17 +21,23 @@ def is_widget(request: Request) -> bool: ) -def context_processor(request: Request) -> dict: +def base_context_processor(request: Request) -> dict: return { "is_widget": is_widget(request), + "provider": request.query_params.get("provider"), } -def build_templates(templates_dir: Path | None = None, filters: dict | None = None) -> Jinja2Templates: +def build_templates( + templates_dir: Path | None = None, filters: dict | None = None, context_processor: ContextProcessor | None = None +) -> Jinja2Templates: directory = [Path(__file__).parent.parent / "templates"] if templates_dir: directory.append(templates_dir) - templates = Jinja2Templates(directory=directory, context_processors=[context_processor]) + context_processors: list[ContextProcessor] = [base_context_processor] + if context_processor: + context_processors.append(context_processor) + templates = Jinja2Templates(directory=directory, context_processors=context_processors) templates.env.globals.update( { "_": _, diff --git a/gallery/easel/route/view/schedule/__init__.py b/gallery/easel/route/view/schedule/__init__.py index a2c5737..13c64a9 100644 --- a/gallery/easel/route/view/schedule/__init__.py +++ b/gallery/easel/route/view/schedule/__init__.py @@ -1,16 +1,32 @@ import datetime from pathlib import Path +from typing import Annotated -from fastapi import APIRouter +from fastapi import APIRouter, Depends from fastapi.responses import HTMLResponse, RedirectResponse from gallery.easel.core import AppRequest +from gallery.sketch.schedule.api import ScheduleApi from gallery.sketch.schedule.catalog import BUNDLE from ..common.utils.tag import TagType, TagUtil from ..common.utils.template import build_templates from .filters import timedelta_format + +async def get_schedule_api(request: AppRequest, provider: str | None = None) -> ScheduleApi: + return request.app.state.api.get_schedule(provider) + + +ScheduleApiDepends = Annotated[ScheduleApi, Depends(get_schedule_api)] + + +def context_procesor(request: AppRequest) -> dict: + return { + "providers": request.app.state.api.get_api_providers(ScheduleApi.TYPE), + } + + templates = build_templates( Path(__file__).parent / "templates", { @@ -18,12 +34,11 @@ templates = build_templates( }, ) -router = APIRouter() +router = APIRouter(prefix="/schedule") -@router.get("/schedule", response_class=HTMLResponse) -async def get_schedule_list(request: AppRequest): - schedule_api = request.app.state.api.schedule +@router.get("/", response_class=HTMLResponse) +async def get_schedule_list(request: AppRequest, schedule_api: ScheduleApiDepends): channels = await schedule_api.get_channels() channels_data = BUNDLE.select_items(channels) return templates.TemplateResponse( @@ -35,10 +50,9 @@ async def get_schedule_list(request: AppRequest): ) -@router.get("/schedule/tag/{tag}", response_class=HTMLResponse) -async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False): +@router.get("/tag/{tag}", response_class=HTMLResponse) +async def get_schedule_tag(request: AppRequest, schedule_api: ScheduleApiDepends, tag: str, live: bool = False): tag_value = TagUtil.parse_tag(tag) - schedule_api = request.app.state.api.schedule results = await schedule_api.get_all_schedules(tag_value.date) return templates.TemplateResponse( request=request, @@ -53,15 +67,14 @@ async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False): ) -@router.get("/schedule/{channel}", response_class=RedirectResponse) +@router.get("/{channel}", response_class=RedirectResponse) async def get_channel_default(channel: str): return RedirectResponse(f"{channel}/tag/today") -@router.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse) -async def get_channel_tag(request: AppRequest, channel: str, tag: str): +@router.get("/{channel}/tag/{tag}", response_class=HTMLResponse) +async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends, channel: str, tag: str): tag_value = TagUtil.parse_tag(tag) - schedule_api = request.app.state.api.schedule if tag_value.type == TagType.DAY: response = await schedule_api.get_channel_schedule(channel, tag_value.date) else: diff --git a/gallery/easel/route/view/schedule/templates/index.html b/gallery/easel/route/view/schedule/templates/index.html index 4763633..0a93198 100644 --- a/gallery/easel/route/view/schedule/templates/index.html +++ b/gallery/easel/route/view/schedule/templates/index.html @@ -4,12 +4,12 @@ {% block content %}

{{_("TV program")}}

- Все {% for channel in channels %} - {{channel.name}} diff --git a/gallery/easel/route/view/weather/__init__.py b/gallery/easel/route/view/weather/__init__.py index fbff432..7559cf5 100644 --- a/gallery/easel/route/view/weather/__init__.py +++ b/gallery/easel/route/view/weather/__init__.py @@ -1,22 +1,39 @@ import datetime from pathlib import Path +from typing import Annotated -from fastapi import APIRouter +from fastapi import APIRouter, Depends from fastapi.responses import HTMLResponse, RedirectResponse from gallery.easel.core import AppRequest +from gallery.sketch.weather.api import WeatherApi from gallery.sketch.weather.model import WeatherResponse from ..common.utils.tag import TagType, TagUtil from ..common.utils.template import build_templates from .filters import cloudness_icon, wind_direction_icon + +async def get_weather_api(request: AppRequest, provider: str | None = None) -> WeatherApi: + return request.app.state.api.get_weather(provider) + + +WeatherApiDepends = Annotated[WeatherApi, Depends(get_weather_api)] + + +def context_procesor(request: AppRequest) -> dict: + return { + "providers": request.app.state.api.get_api_providers(WeatherApi.TYPE), + } + + templates = build_templates( Path(__file__).parent / "templates", { "wind_direction_icon": wind_direction_icon, "cloudness_icon": cloudness_icon, }, + context_procesor, ) @@ -30,12 +47,11 @@ def build_weather_response(request: AppRequest, response: WeatherResponse): ) -router = APIRouter() +router = APIRouter(prefix="/weather") -@router.get("/weather", response_class=HTMLResponse) -async def get_weather_index(request: AppRequest, query: str | None = None): - weather_api = request.app.state.api.weather +@router.get("/", response_class=HTMLResponse) +async def get_weather_index(request: AppRequest, weather_api: WeatherApiDepends, query: str | None = None): locations = (await weather_api.find_locations(query)) if query else [] return templates.TemplateResponse( request=request, @@ -46,29 +62,31 @@ async def get_weather_index(request: AppRequest, query: str | None = None): ) -@router.get("/weather/{location}", response_class=RedirectResponse) +@router.get("/{location}", response_class=RedirectResponse) async def get_weather_default(location: str): return RedirectResponse(f"{location}/tag/today") -@router.get("/weather/{location}/day/{date}", response_class=HTMLResponse) -async def get_weather_day(request: AppRequest, location: str, date: datetime.date): - weather_api = request.app.state.api.weather +@router.get("/{location}/day/{date}", response_class=HTMLResponse) +async def get_weather_day( + request: AppRequest, + weather_api: WeatherApiDepends, + location: str, + date: datetime.date, +): response = await weather_api.get_day(location, date) return build_weather_response(request, response) -@router.get("/weather/{location}/days/{days}", response_class=HTMLResponse) -async def get_weather_days(request: AppRequest, location: str, days: int): - weather_api = request.app.state.api.weather +@router.get("/{location}/days/{days}", response_class=HTMLResponse) +async def get_weather_days(request: AppRequest, weather_api: WeatherApiDepends, location: str, days: int): response = await weather_api.get_days(location, days) return build_weather_response(request, response) -@router.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse) -async def get_weather_tag(request: AppRequest, location: str, tag: str): +@router.get("/{location}/tag/{tag}", response_class=HTMLResponse) +async def get_weather_tag(request: AppRequest, weather_api: WeatherApiDepends, location: str, tag: str): tag_value = TagUtil.parse_tag(tag) - weather_api = request.app.state.api.weather if tag_value.type == TagType.DAY: response = await weather_api.get_day(location, tag_value.date) elif tag_value.type == TagType.DAYS: diff --git a/gallery/easel/route/view/weather/templates/index.html b/gallery/easel/route/view/weather/templates/index.html index 7a6a2b5..0b07cc6 100644 --- a/gallery/easel/route/view/weather/templates/index.html +++ b/gallery/easel/route/view/weather/templates/index.html @@ -1,6 +1,23 @@ {% extends "base.html" %} {% block title %}{{_("Weather")}}{% endblock %} +{# {% block header %} + +{% endblock %} #} + {% block content %}

{{_("Weather")}}

{% for location in locations %} - @@ -39,7 +56,7 @@ container.innerHTML = ''; for (const [id, name] of Object.entries(locations)) { const element = document.createElement('a'); - element.href = `weather/${id}`; + element.href = `${id}`; element.className = 'list-group-item list-group-item-action px-4 d-flex justify-content-between align-items-start'; element.innerHTML = ` ${name} diff --git a/gallery/sketch/api.py b/gallery/sketch/api.py index 3ee96ea..4e4ecf9 100644 --- a/gallery/sketch/api.py +++ b/gallery/sketch/api.py @@ -2,8 +2,13 @@ from typing import TypeVar class Api: + TYPE: str PROVIDER: str + @property + def type(self) -> str: + return self.TYPE + @property def provider(self) -> str: return self.PROVIDER diff --git a/gallery/sketch/bundle.py b/gallery/sketch/bundle.py index f9a7de6..02dff60 100644 --- a/gallery/sketch/bundle.py +++ b/gallery/sketch/bundle.py @@ -3,26 +3,27 @@ from .schedule.api import ScheduleApi from .weather.api import WeatherApi -class ApiBundle(list[Api]): - def __init__(self, values: list[Api]) -> None: - super().__init__(values) +class ApiBundle: + def __init__(self, values: list[Api]): + self._values = values + self._by_provider = {value.provider: value for value in values} - def get_api_by_provider(self, provider: str) -> Api: - for value in self: - if value.PROVIDER == provider: - return value - raise ValueError(provider) + def get_api_providers(self, api_type: str) -> list[str]: + result = [] + for value in self._values: + if value.type == api_type: + result.append(value.provider) + return result - def get_api_by_type(self, api_type: type[API]) -> API: - for value in self: + def get_api(self, api_type: type[API], provider: str | None = None) -> API: + for value in self._values: if isinstance(value, api_type): - return value + if provider is None or provider == value.provider: + return value raise ValueError(api_type) - @property - def weather(self) -> WeatherApi: - return self.get_api_by_type(WeatherApi) + def get_weather(self, provider: str | None = None) -> WeatherApi: + return self.get_api(WeatherApi, provider) - @property - def schedule(self) -> ScheduleApi: - return self.get_api_by_type(ScheduleApi) + def get_schedule(self, provider: str | None = None) -> ScheduleApi: + return self.get_api(ScheduleApi, provider) diff --git a/gallery/sketch/schedule/api.py b/gallery/sketch/schedule/api.py index e7e8e4f..11abe2a 100644 --- a/gallery/sketch/schedule/api.py +++ b/gallery/sketch/schedule/api.py @@ -6,6 +6,7 @@ from .model import ChannelId, Schedule class ScheduleApi(Api): + TYPE = "schedule" INTERVAL: float = 0.5 async def get_channels(self) -> list[ChannelId]: diff --git a/gallery/sketch/schedule/cached.py b/gallery/sketch/schedule/cached.py index 47cb410..ec36150 100644 --- a/gallery/sketch/schedule/cached.py +++ b/gallery/sketch/schedule/cached.py @@ -12,7 +12,7 @@ CACHE_PRESET = CachePreset(ttl=TimeUnit.HOUR * 6) class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]): - CACHE_KEY = "schedule" + CACHE_KEY = ScheduleApi.TYPE @cached( key_builder=lambda fun, self: f"api.{self.CACHE_KEY}.{self.provider}.channels", diff --git a/gallery/sketch/weather/api.py b/gallery/sketch/weather/api.py index 9a6159e..0196dd8 100644 --- a/gallery/sketch/weather/api.py +++ b/gallery/sketch/weather/api.py @@ -5,6 +5,7 @@ from .model import Location, WeatherResponse class WeatherApi(Api): + TYPE = "weather" async def find_locations(self, query: str) -> list[Location]: raise NotImplementedError diff --git a/gallery/sketch/weather/cached.py b/gallery/sketch/weather/cached.py index c32feb8..f972e94 100644 --- a/gallery/sketch/weather/cached.py +++ b/gallery/sketch/weather/cached.py @@ -11,7 +11,7 @@ CACHE_PRESET = DEFAULT_CACHE_PRESET class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]): - CACHE_KEY = "weather" + CACHE_KEY = WeatherApi.TYPE @cached( key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.locations.{query}",