Compare commits
1 Commits
0.3.4
...
8b53aed18b
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b53aed18b |
@@ -8,6 +8,11 @@ from gallery.sketch.weather.model import Location, WeatherResponse
|
|||||||
router = APIRouter(prefix="/weather")
|
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")
|
@router.get("/locations")
|
||||||
async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]:
|
async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]:
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import typing
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from babel.dates import format_date
|
from babel.dates import format_date
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from gallery.easel.core import AppRequest
|
||||||
from gallery.version import __version__
|
from gallery.version import __version__
|
||||||
|
|
||||||
from ...translation import _
|
from ...translation import _
|
||||||
from .tag import TagUtil
|
from .tag import TagUtil
|
||||||
|
|
||||||
|
ContextProcessor = typing.Callable[[AppRequest], dict[str, typing.Any]]
|
||||||
|
|
||||||
|
|
||||||
def is_widget(request: Request) -> bool:
|
def is_widget(request: Request) -> bool:
|
||||||
return (request.url.hostname and request.url.hostname.startswith("weather")) or (
|
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 {
|
return {
|
||||||
"is_widget": is_widget(request),
|
"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"]
|
directory = [Path(__file__).parent.parent / "templates"]
|
||||||
if templates_dir:
|
if templates_dir:
|
||||||
directory.append(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(
|
templates.env.globals.update(
|
||||||
{
|
{
|
||||||
"_": _,
|
"_": _,
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from gallery.easel.core import AppRequest
|
from gallery.easel.core import AppRequest
|
||||||
|
from gallery.sketch.schedule.api import ScheduleApi
|
||||||
from gallery.sketch.schedule.catalog import BUNDLE
|
from gallery.sketch.schedule.catalog import BUNDLE
|
||||||
|
|
||||||
from ..common.utils.tag import TagType, TagUtil
|
from ..common.utils.tag import TagType, TagUtil
|
||||||
from ..common.utils.template import build_templates
|
from ..common.utils.template import build_templates
|
||||||
from .filters import timedelta_format
|
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(
|
templates = build_templates(
|
||||||
Path(__file__).parent / "templates",
|
Path(__file__).parent / "templates",
|
||||||
{
|
{
|
||||||
@@ -18,12 +34,11 @@ templates = build_templates(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(prefix="/schedule")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/schedule", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
async def get_schedule_list(request: AppRequest):
|
async def get_schedule_list(request: AppRequest, schedule_api: ScheduleApiDepends):
|
||||||
schedule_api = request.app.state.api.schedule
|
|
||||||
channels = await schedule_api.get_channels()
|
channels = await schedule_api.get_channels()
|
||||||
channels_data = BUNDLE.select_items(channels)
|
channels_data = BUNDLE.select_items(channels)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
@@ -35,10 +50,9 @@ async def get_schedule_list(request: AppRequest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/schedule/tag/{tag}", response_class=HTMLResponse)
|
@router.get("/tag/{tag}", response_class=HTMLResponse)
|
||||||
async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False):
|
async def get_schedule_tag(request: AppRequest, schedule_api: ScheduleApiDepends, tag: str, live: bool = False):
|
||||||
tag_value = TagUtil.parse_tag(tag)
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
schedule_api = request.app.state.api.schedule
|
|
||||||
results = await schedule_api.get_all_schedules(tag_value.date)
|
results = await schedule_api.get_all_schedules(tag_value.date)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
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):
|
async def get_channel_default(channel: str):
|
||||||
return RedirectResponse(f"{channel}/tag/today")
|
return RedirectResponse(f"{channel}/tag/today")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse)
|
@router.get("/{channel}/tag/{tag}", response_class=HTMLResponse)
|
||||||
async def get_channel_tag(request: AppRequest, channel: str, tag: str):
|
async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends, channel: str, tag: str):
|
||||||
tag_value = TagUtil.parse_tag(tag)
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
schedule_api = request.app.state.api.schedule
|
|
||||||
if tag_value.type == TagType.DAY:
|
if tag_value.type == TagType.DAY:
|
||||||
response = await schedule_api.get_channel_schedule(channel, tag_value.date)
|
response = await schedule_api.get_channel_schedule(channel, tag_value.date)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{_("TV program")}}</h1>
|
<h1>{{_("TV program")}}</h1>
|
||||||
<div class="list-group mb-5">
|
<div class="list-group mb-5">
|
||||||
<a href="schedule/tag/today"
|
<a href="tag/today"
|
||||||
class="list-group-item list-group-item-action px-4">
|
class="list-group-item list-group-item-action px-4">
|
||||||
<span class="fw-bold">Все</span>
|
<span class="fw-bold">Все</span>
|
||||||
</a>
|
</a>
|
||||||
{% for channel in channels %}
|
{% for channel in channels %}
|
||||||
<a href="schedule/{{channel.id}}"
|
<a href="{{channel.id}}"
|
||||||
class="list-group-item list-group-item-action px-4">
|
class="list-group-item list-group-item-action px-4">
|
||||||
<span class="text-primary">{{channel.name}}</span>
|
<span class="text-primary">{{channel.name}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from gallery.easel.core import AppRequest
|
from gallery.easel.core import AppRequest
|
||||||
|
from gallery.sketch.weather.api import WeatherApi
|
||||||
from gallery.sketch.weather.model import WeatherResponse
|
from gallery.sketch.weather.model import WeatherResponse
|
||||||
|
|
||||||
from ..common.utils.tag import TagType, TagUtil
|
from ..common.utils.tag import TagType, TagUtil
|
||||||
from ..common.utils.template import build_templates
|
from ..common.utils.template import build_templates
|
||||||
from .filters import cloudness_icon, wind_direction_icon
|
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(
|
templates = build_templates(
|
||||||
Path(__file__).parent / "templates",
|
Path(__file__).parent / "templates",
|
||||||
{
|
{
|
||||||
"wind_direction_icon": wind_direction_icon,
|
"wind_direction_icon": wind_direction_icon,
|
||||||
"cloudness_icon": cloudness_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)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
async def get_weather_index(request: AppRequest, query: str | None = None):
|
async def get_weather_index(request: AppRequest, weather_api: WeatherApiDepends, query: str | None = None):
|
||||||
weather_api = request.app.state.api.weather
|
|
||||||
locations = (await weather_api.find_locations(query)) if query else []
|
locations = (await weather_api.find_locations(query)) if query else []
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
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):
|
async def get_weather_default(location: str):
|
||||||
return RedirectResponse(f"{location}/tag/today")
|
return RedirectResponse(f"{location}/tag/today")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
|
@router.get("/{location}/day/{date}", response_class=HTMLResponse)
|
||||||
async def get_weather_day(request: AppRequest, location: str, date: datetime.date):
|
async def get_weather_day(
|
||||||
weather_api = request.app.state.api.weather
|
request: AppRequest,
|
||||||
|
weather_api: WeatherApiDepends,
|
||||||
|
location: str,
|
||||||
|
date: datetime.date,
|
||||||
|
):
|
||||||
response = await weather_api.get_day(location, date)
|
response = await weather_api.get_day(location, date)
|
||||||
return build_weather_response(request, response)
|
return build_weather_response(request, response)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
|
@router.get("/{location}/days/{days}", response_class=HTMLResponse)
|
||||||
async def get_weather_days(request: AppRequest, location: str, days: int):
|
async def get_weather_days(request: AppRequest, weather_api: WeatherApiDepends, location: str, days: int):
|
||||||
weather_api = request.app.state.api.weather
|
|
||||||
response = await weather_api.get_days(location, days)
|
response = await weather_api.get_days(location, days)
|
||||||
return build_weather_response(request, response)
|
return build_weather_response(request, response)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse)
|
@router.get("/{location}/tag/{tag}", response_class=HTMLResponse)
|
||||||
async def get_weather_tag(request: AppRequest, location: str, tag: str):
|
async def get_weather_tag(request: AppRequest, weather_api: WeatherApiDepends, location: str, tag: str):
|
||||||
tag_value = TagUtil.parse_tag(tag)
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
weather_api = request.app.state.api.weather
|
|
||||||
if tag_value.type == TagType.DAY:
|
if tag_value.type == TagType.DAY:
|
||||||
response = await weather_api.get_day(location, tag_value.date)
|
response = await weather_api.get_day(location, tag_value.date)
|
||||||
elif tag_value.type == TagType.DAYS:
|
elif tag_value.type == TagType.DAYS:
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{_("Weather")}}{% endblock %}
|
{% block title %}{{_("Weather")}}{% endblock %}
|
||||||
|
|
||||||
|
{# {% block header %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false">
|
||||||
|
{{provider or providers[0]}}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for provider in providers %}
|
||||||
|
<li><a class="dropdown-item"
|
||||||
|
href="{{ url_for('get_weather_index').include_query_params(provider=provider) }}">{{provider}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %} #}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{_("Weather")}}</h1>
|
<h1>{{_("Weather")}}</h1>
|
||||||
<form action=""
|
<form action=""
|
||||||
@@ -19,7 +36,7 @@
|
|||||||
<ul id="locations"
|
<ul id="locations"
|
||||||
class="list-group mb-5">
|
class="list-group mb-5">
|
||||||
{% for location in locations %}
|
{% for location in locations %}
|
||||||
<a href="weather/{{location.id}}"
|
<a href="{{location.id}}"
|
||||||
class="list-group-item list-group-item-action px-4"
|
class="list-group-item list-group-item-action px-4"
|
||||||
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
|
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
|
||||||
<span class="fi fi-{{location.country_code}} me-1"></span>
|
<span class="fi fi-{{location.country_code}} me-1"></span>
|
||||||
@@ -39,7 +56,7 @@
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
for (const [id, name] of Object.entries(locations)) {
|
for (const [id, name] of Object.entries(locations)) {
|
||||||
const element = document.createElement('a');
|
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.className = 'list-group-item list-group-item-action px-4 d-flex justify-content-between align-items-start';
|
||||||
element.innerHTML = `
|
element.innerHTML = `
|
||||||
<span class="text-primary me-auto">${name}</span>
|
<span class="text-primary me-auto">${name}</span>
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ from typing import TypeVar
|
|||||||
|
|
||||||
|
|
||||||
class Api:
|
class Api:
|
||||||
|
TYPE: str
|
||||||
PROVIDER: str
|
PROVIDER: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> str:
|
||||||
|
return self.TYPE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def provider(self) -> str:
|
def provider(self) -> str:
|
||||||
return self.PROVIDER
|
return self.PROVIDER
|
||||||
|
|||||||
@@ -3,26 +3,27 @@ from .schedule.api import ScheduleApi
|
|||||||
from .weather.api import WeatherApi
|
from .weather.api import WeatherApi
|
||||||
|
|
||||||
|
|
||||||
class ApiBundle(list[Api]):
|
class ApiBundle:
|
||||||
def __init__(self, values: list[Api]) -> None:
|
def __init__(self, values: list[Api]):
|
||||||
super().__init__(values)
|
self._values = values
|
||||||
|
self._by_provider = {value.provider: value for value in values}
|
||||||
|
|
||||||
def get_api_by_provider(self, provider: str) -> Api:
|
def get_api_providers(self, api_type: str) -> list[str]:
|
||||||
for value in self:
|
result = []
|
||||||
if value.PROVIDER == provider:
|
for value in self._values:
|
||||||
return value
|
if value.type == api_type:
|
||||||
raise ValueError(provider)
|
result.append(value.provider)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_api_by_type(self, api_type: type[API]) -> API:
|
def get_api(self, api_type: type[API], provider: str | None = None) -> API:
|
||||||
for value in self:
|
for value in self._values:
|
||||||
if isinstance(value, api_type):
|
if isinstance(value, api_type):
|
||||||
return value
|
if provider is None or provider == value.provider:
|
||||||
|
return value
|
||||||
raise ValueError(api_type)
|
raise ValueError(api_type)
|
||||||
|
|
||||||
@property
|
def get_weather(self, provider: str | None = None) -> WeatherApi:
|
||||||
def weather(self) -> WeatherApi:
|
return self.get_api(WeatherApi, provider)
|
||||||
return self.get_api_by_type(WeatherApi)
|
|
||||||
|
|
||||||
@property
|
def get_schedule(self, provider: str | None = None) -> ScheduleApi:
|
||||||
def schedule(self) -> ScheduleApi:
|
return self.get_api(ScheduleApi, provider)
|
||||||
return self.get_api_by_type(ScheduleApi)
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from .model import ChannelId, Schedule
|
|||||||
|
|
||||||
|
|
||||||
class ScheduleApi(Api):
|
class ScheduleApi(Api):
|
||||||
|
TYPE = "schedule"
|
||||||
INTERVAL: float = 0.5
|
INTERVAL: float = 0.5
|
||||||
|
|
||||||
async def get_channels(self) -> list[ChannelId]:
|
async def get_channels(self) -> list[ChannelId]:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ CACHE_PRESET = CachePreset(ttl=TimeUnit.HOUR * 6)
|
|||||||
|
|
||||||
|
|
||||||
class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
|
class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
|
||||||
CACHE_KEY = "schedule"
|
CACHE_KEY = ScheduleApi.TYPE
|
||||||
|
|
||||||
@cached(
|
@cached(
|
||||||
key_builder=lambda fun, self: f"api.{self.CACHE_KEY}.{self.provider}.channels",
|
key_builder=lambda fun, self: f"api.{self.CACHE_KEY}.{self.provider}.channels",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from .model import Location, WeatherResponse
|
|||||||
|
|
||||||
|
|
||||||
class WeatherApi(Api):
|
class WeatherApi(Api):
|
||||||
|
TYPE = "weather"
|
||||||
|
|
||||||
async def find_locations(self, query: str) -> list[Location]:
|
async def find_locations(self, query: str) -> list[Location]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ CACHE_PRESET = DEFAULT_CACHE_PRESET
|
|||||||
|
|
||||||
|
|
||||||
class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]):
|
class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]):
|
||||||
CACHE_KEY = "weather"
|
CACHE_KEY = WeatherApi.TYPE
|
||||||
|
|
||||||
@cached(
|
@cached(
|
||||||
key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.locations.{query}",
|
key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.locations.{query}",
|
||||||
|
|||||||
Reference in New Issue
Block a user